Compare commits
9
Commits
b6e7fdc289
..
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d75f4d22 | ||
|
|
588d2dcc36 | ||
|
|
4ed3481166 | ||
|
|
8805a8f183 | ||
|
|
d7cbab3e4c | ||
|
|
1ea4bf6480 | ||
|
|
f689023089 | ||
|
|
575d26a771 | ||
|
|
4d78785f0b |
@@ -29,6 +29,7 @@ add_library(soothe2_dsp SHARED
|
|||||||
fn529fe0.cpp
|
fn529fe0.cpp
|
||||||
rt_weights.cpp
|
rt_weights.cpp
|
||||||
rt_mask_tables.cpp
|
rt_mask_tables.cpp
|
||||||
|
log2_ln.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(soothe2_harness harness.cpp)
|
add_executable(soothe2_harness harness.cpp)
|
||||||
|
|||||||
+85
@@ -95,4 +95,89 @@ void execute(const FFTPlan* plan, std::complex<double>* buf) {
|
|||||||
execute_forward(plan, buf);
|
execute_forward(plan, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void execute_real_forward(const FFTPlan* plan, double* real_in, std::complex<double>* complex_out) {
|
||||||
|
// Forward real RFFT: N real → N/2+1 complex
|
||||||
|
// Algorithm: Pack N real as N/2 complex, do complex FFT of size N/2, unpack
|
||||||
|
uint32_t N = plan->N;
|
||||||
|
uint32_t half = N / 2;
|
||||||
|
|
||||||
|
// Pack N real as N/2 complex: z[k] = x[2k] + i*x[2k+1]
|
||||||
|
std::vector<std::complex<double>> z(half);
|
||||||
|
for (uint32_t k = 0; k < half; k++) {
|
||||||
|
z[k] = std::complex<double>(real_in[2*k], real_in[2*k + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a plan for N/2
|
||||||
|
FFTPlan half_plan;
|
||||||
|
init_plan(&half_plan, plan->log2N - 1);
|
||||||
|
|
||||||
|
// Complex FFT of z (size N/2)
|
||||||
|
execute_forward(&half_plan, z.data());
|
||||||
|
|
||||||
|
// Unpack to get N/2+1 complex output
|
||||||
|
// Using the formula: X[k] = 0.5 * (Z[k] + Z*[N/2-k]) - 0.5i*exp(-2*pi*i*k/N) * (Z[k] - Z*[N/2-k])
|
||||||
|
complex_out[0] = std::complex<double>(z[0].real() + z[0].imag(), 0.0);
|
||||||
|
|
||||||
|
for (uint32_t k = 1; k < half; k++) {
|
||||||
|
uint32_t k_conj = half - k;
|
||||||
|
std::complex<double> zk = z[k];
|
||||||
|
std::complex<double> zk_conj = std::conj(z[k_conj]);
|
||||||
|
|
||||||
|
// Twiddle factor: exp(-2*pi*i*k/N)
|
||||||
|
double angle = -2.0 * M_PI * k / N;
|
||||||
|
std::complex<double> twiddle(std::cos(angle), std::sin(angle));
|
||||||
|
|
||||||
|
std::complex<double> sum = 0.5 * (zk + zk_conj);
|
||||||
|
std::complex<double> diff = std::complex<double>(0.0, -0.5) * twiddle * (zk - zk_conj);
|
||||||
|
|
||||||
|
complex_out[k] = sum + diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nyquist frequency
|
||||||
|
complex_out[half] = std::complex<double>(z[0].real() - z[0].imag(), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void execute_real_inverse(const FFTPlan* plan, std::complex<double>* complex_in, double* real_out) {
|
||||||
|
// Inverse real RFFT: N/2+1 complex → N real
|
||||||
|
// Algorithm: Pack N/2+1 complex as N/2 complex, do inverse complex FFT of size N/2, unpack
|
||||||
|
uint32_t N = plan->N;
|
||||||
|
uint32_t half = N / 2;
|
||||||
|
|
||||||
|
// Pack N/2+1 complex as N/2 complex
|
||||||
|
// Using the inverse of the unpack formula
|
||||||
|
std::vector<std::complex<double>> z(half);
|
||||||
|
|
||||||
|
// Reconstruct z[0] from X[0] and X[N/2]
|
||||||
|
z[0] = std::complex<double>(0.5 * (complex_in[0].real() + complex_in[half].real()),
|
||||||
|
0.5 * (complex_in[0].real() - complex_in[half].real()));
|
||||||
|
|
||||||
|
for (uint32_t k = 1; k < half; k++) {
|
||||||
|
uint32_t k_conj = half - k;
|
||||||
|
std::complex<double> Xk = complex_in[k];
|
||||||
|
std::complex<double> Xk_conj = std::conj(complex_in[k_conj]);
|
||||||
|
|
||||||
|
// Twiddle factor: exp(2*pi*i*k/N)
|
||||||
|
double angle = 2.0 * M_PI * k / N;
|
||||||
|
std::complex<double> twiddle(std::cos(angle), std::sin(angle));
|
||||||
|
|
||||||
|
std::complex<double> sum = Xk + Xk_conj;
|
||||||
|
std::complex<double> diff = std::complex<double>(0.0, 1.0) * twiddle * (Xk - Xk_conj);
|
||||||
|
|
||||||
|
z[k] = 0.5 * (sum + diff);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a plan for N/2
|
||||||
|
FFTPlan half_plan;
|
||||||
|
init_plan(&half_plan, plan->log2N - 1);
|
||||||
|
|
||||||
|
// Inverse complex FFT (size N/2)
|
||||||
|
execute_inverse(&half_plan, z.data());
|
||||||
|
|
||||||
|
// Unpack to N real
|
||||||
|
for (uint32_t k = 0; k < half; k++) {
|
||||||
|
real_out[2*k] = z[k].real();
|
||||||
|
real_out[2*k + 1] = z[k].imag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,9 @@ void build_twiddle(FFTPlan* plan, double* scratch);
|
|||||||
void execute(const FFTPlan* plan, std::complex<double>* buf);
|
void execute(const FFTPlan* plan, std::complex<double>* buf);
|
||||||
void execute_inverse(const FFTPlan* plan, std::complex<double>* buf);
|
void execute_inverse(const FFTPlan* plan, std::complex<double>* buf);
|
||||||
|
|
||||||
|
// Real RFFT: N real → N/2+1 complex (forward)
|
||||||
|
// N/2+1 complex → N real (inverse)
|
||||||
|
void execute_real_forward(const FFTPlan* plan, double* real_in, std::complex<double>* complex_out);
|
||||||
|
void execute_real_inverse(const FFTPlan* plan, std::complex<double>* complex_in, double* real_out);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,177 @@
|
|||||||
// Structural mask-apply chain FUN_180529fe0 (mono path). Step-by-step
|
// 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
|
// 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).
|
// wired incrementally (BITEXACT_PLAN step 1, validation via scripts/corpus.py).
|
||||||
|
//
|
||||||
|
// Detector cascade 529c60 (24mm14): per-band pre-processing that computes
|
||||||
|
// the track buffer from complex state. Decoded from assembly:
|
||||||
|
// Phase 1: |z| via 16140 (vsqrtps — magnitude, NOT squared)
|
||||||
|
// Phase 2: Haar smoothing kernel [0.25, 0.5, 0.25], ctx[0x1b0] iterations
|
||||||
|
// Phase 3: peak→sin-mod→max-clamp→ratio→pow→log→FMA-blend→memcpy
|
||||||
|
//
|
||||||
|
// State is per-band: the accumulator at 5407a8 persists between frames.
|
||||||
|
|
||||||
namespace fn529fe0 {
|
namespace fn529fe0 {
|
||||||
|
|
||||||
|
// ---- Detector cascade 529c60 -----------------------------------------------
|
||||||
|
|
||||||
|
// One Haar smoothing pass (kernel [0.25, 0.5, 0.25]).
|
||||||
|
// Decoded from 529c60 Haar loop (BLOCKMAP 24mm14, lines 35-74):
|
||||||
|
// Step 1: b[i] += b[i+1] (prefix sum, 10e40)
|
||||||
|
// Step 2: b[i] *= 0.5 (scalar mul, ffe0)
|
||||||
|
// Step 3: scratch[i] = b[i+1] + b[i] (3-op add, 11580)
|
||||||
|
// Step 4: b[i+1] = 0.5 * scratch[i] (scalar mul+store, 4720)
|
||||||
|
// Net effect: b[0]=0.5*(b0+b1), b[i]=0.25*b[i-1]+0.5*b[i]+0.25*b[i+1], etc.
|
||||||
|
// Implementation follows Python reference exactly (detector_cascade.py).
|
||||||
|
void haar_one_pass(float* b, size_t n) {
|
||||||
|
if (n < 2) return;
|
||||||
|
// Steps 1+2: b[i] = 0.5*(b[i]+b[i+1]) for i in [0, n-2]
|
||||||
|
for (size_t i = 0; i < n - 1; i++) {
|
||||||
|
b[i] = 0.5f * (b[i] + b[i + 1]);
|
||||||
|
}
|
||||||
|
// Steps 3+4: b[i+1] = 0.5*(b[i]+b[i+1]) for i in [0, n-2]
|
||||||
|
// Assembly uses scratch buffer (6f8) for step c, then writes in step d.
|
||||||
|
// Equivalent: iterate backwards so b[i] is read before being overwritten.
|
||||||
|
for (size_t i = n - 1; i > 0; i--) {
|
||||||
|
b[i] = 0.5f * (b[i - 1] + b[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Haar smoothing: iterate Haar passes. ctx[0x1b0] iterations.
|
||||||
|
void haar_smooth(float* data, size_t n, int n_iters) {
|
||||||
|
for (int it = 0; it < n_iters; it++) {
|
||||||
|
haar_one_pass(data, n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute |z| from interleaved complex state (Phase 1, 16140).
|
||||||
|
// in: interleaved [re0,im0,re1,im1,...], out: [mag0,mag1,...]
|
||||||
|
// Uses vsqrtps in assembly (NOT vmultps — magnitude, NOT squared).
|
||||||
|
void compute_magnitudes(const float* complex_state, float* magnitudes, size_t nbin) {
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
float re = complex_state[2 * i];
|
||||||
|
float im = complex_state[2 * i + 1];
|
||||||
|
magnitudes[i] = std::sqrt(re * re + im * im);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full detector cascade 529c60 (decoded from assembly, 24mm14).
|
||||||
|
//
|
||||||
|
// Pipeline:
|
||||||
|
// 1. compute_magnitudes (Phase 1, 16140): complex → |z|
|
||||||
|
// 2. haar_smooth (Phase 2): |z| → smoothed curve
|
||||||
|
// 3. peak = max(curve) (4d56b0)
|
||||||
|
// 4. sin_peak = sin(param*30 - 90) * 0.115129 * peak (1a14cac CRT sin)
|
||||||
|
// 5. curve[i] = max(curve[i], sin_peak) (52d8a0→10860)
|
||||||
|
// 6. ratio = (ctx24 / ctx1a0) * ctx1ac
|
||||||
|
// 7. r = ratio * 0.001
|
||||||
|
// 8. inner = pow(50, r) * r
|
||||||
|
// 9. w = -log10(inner)
|
||||||
|
// 10. acc[i] = acc[i] * w + curve[i] * (1-w) (blend)
|
||||||
|
// 11. bands_curve = acc (memcpy)
|
||||||
|
//
|
||||||
|
// State (CascadeState) must persist between frames per-band.
|
||||||
|
// Complex state is interleaved re/im with length 2*nbin.
|
||||||
|
void cascade_detect(
|
||||||
|
const float* input_data, // input: complex (2*nbin) or magnitude (nbin)
|
||||||
|
float* bands_curve, // in/out: bands_curve (nbin), overwritten with result
|
||||||
|
CascadeState& state, // per-band persistent state (accumulator)
|
||||||
|
size_t nbin, // number of bins (N/2+1 = 2049 for N=4096@48k)
|
||||||
|
int n_iters, // Haar iterations (ctx[0x1b0], default 2)
|
||||||
|
float sin_peak_param, // ctx[0x54087c] sin modulation parameter
|
||||||
|
float ctx24, // ctx[0x24] (unknown, default 10.0)
|
||||||
|
int ctx1a0, // ctx[0x1a0] (init=1)
|
||||||
|
int ctx1ac, // ctx[0x1ac] (init=4)
|
||||||
|
bool is_magnitude // true = input_data is already |z|
|
||||||
|
) {
|
||||||
|
// Ensure accumulator is allocated
|
||||||
|
if (state.accumulator.size() != nbin) {
|
||||||
|
state.accumulator.assign(nbin, 0.0f);
|
||||||
|
}
|
||||||
|
float* acc = state.accumulator.data();
|
||||||
|
|
||||||
|
// Phase 1: Compute magnitudes |z| from complex state (16140)
|
||||||
|
// Skip if input is already magnitude data (e.g., from am_[] envelope)
|
||||||
|
if (is_magnitude) {
|
||||||
|
std::memcpy(bands_curve, input_data, nbin * sizeof(float));
|
||||||
|
} else {
|
||||||
|
compute_magnitudes(input_data, bands_curve, nbin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: Haar smoothing (529c60, ctx[0x1b0] iterations)
|
||||||
|
haar_smooth(bands_curve, nbin, n_iters);
|
||||||
|
|
||||||
|
// Phase 3: Post-processing and blend (529c60, lines 74-123)
|
||||||
|
|
||||||
|
// Peak via 4d56b0 (horizontal max of SSE4 loop)
|
||||||
|
float peak = 0.0f;
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
if (bands_curve[i] > peak) peak = bands_curve[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sin-modulated floor (1a14cac CRT sin):
|
||||||
|
// sin_peak = sin(param * 30 - 90) * 0.115129 * peak
|
||||||
|
float sin_peak = 0.0f;
|
||||||
|
if (sin_peak_param != 0.0f) {
|
||||||
|
float angle_deg = sin_peak_param * 30.0f - 90.0f;
|
||||||
|
sin_peak = std::sin(angle_deg * static_cast<float>(M_PI) / 180.0f)
|
||||||
|
* 0.115129f * peak;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp: curve[i] = max(curve[i], sin_peak) (52d8a0→10860)
|
||||||
|
if (sin_peak > 0.0f) {
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
if (bands_curve[i] < sin_peak) bands_curve[i] = sin_peak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight computation from assembly (529e00-529e5e).
|
||||||
|
//
|
||||||
|
// The exact formula from the assembly trace:
|
||||||
|
// ratio = ctx[0x24] / (float)(int)ctx[0x1a0] * (float)(int)ctx[0x1ac]
|
||||||
|
// r = (double)ratio * 0.001
|
||||||
|
// inner = pow(50.0, r) * r (call [IAT 0x181bab3f0])
|
||||||
|
// w = (float)(-log10(inner)) (via cd6(0.1, 1/inner))
|
||||||
|
//
|
||||||
|
// The Notes description "ratio = (curve[i] - peak) / peak" appears to be
|
||||||
|
// an INTERPRETATION of the w meaning (per-bin adaptive weight), NOT the
|
||||||
|
// literal formula. The actual formula uses ctx parameters.
|
||||||
|
//
|
||||||
|
// When peak == 0, skip blend (all zeros → output unchanged).
|
||||||
|
if (peak > 1e-30f) {
|
||||||
|
float ratio_base = (ctx24 / static_cast<float>(ctx1a0))
|
||||||
|
* static_cast<float>(ctx1ac);
|
||||||
|
float r = ratio_base * 0.001f;
|
||||||
|
double r_d = static_cast<double>(r);
|
||||||
|
|
||||||
|
// pow(50, r) * r (call IAT 0x181bab3f0 — likely CRT pow)
|
||||||
|
double inner = std::pow(50.0, r_d) * r_d;
|
||||||
|
|
||||||
|
// w = -log10(inner) (cd6(0.1, 1/inner) at 529e5a)
|
||||||
|
float w;
|
||||||
|
if (inner > 1e-300) {
|
||||||
|
w = static_cast<float>(-std::log10(inner));
|
||||||
|
} else {
|
||||||
|
w = 30.0f; // clamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp w to [0, 1] for stability
|
||||||
|
w = std::min(std::max(w, 0.0f), 1.0f);
|
||||||
|
|
||||||
|
float one_minus_w = 1.0f - w;
|
||||||
|
|
||||||
|
// Blend: acc[i] *= w; acc[i] += curve[i] * (1-w)
|
||||||
|
// 52d920 (scalar mul) + 52dae0 (FMA)
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
acc[i] = acc[i] * w + bands_curve[i] * one_minus_w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy accumulator → bands_curve (52dbc0 memcpy)
|
||||||
|
std::memcpy(bands_curve, acc, nbin * sizeof(float));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Legacy structural chain (pre-cascade) ---------------------------------
|
||||||
|
|
||||||
void iir1(float* x, const double* A, const double* B, size_t nbin, double acc0) {
|
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)
|
// leaky first-order: y = A*acc + B*x ; acc = y (B = 1-A from live tables)
|
||||||
// State persists across calls via static accumulator (per-thread).
|
// State persists across calls via static accumulator (per-thread).
|
||||||
|
|||||||
@@ -18,6 +18,57 @@
|
|||||||
// (level = am/res) but fed through the structural chain instead of the LUT bridge.
|
// (level = am/res) but fed through the structural chain instead of the LUT bridge.
|
||||||
namespace fn529fe0 {
|
namespace fn529fe0 {
|
||||||
|
|
||||||
|
// ---- Detector cascade 529c60 -----------------------------------------------
|
||||||
|
|
||||||
|
// Per-band persistent state for the detector cascade.
|
||||||
|
// The accumulator (5407a8 in the binary) persists between frames,
|
||||||
|
// creating exponential smoothing: acc_{t+1} = w * acc_t + (1-w) * curve_t
|
||||||
|
struct CascadeState {
|
||||||
|
std::vector<float> accumulator; // nbin elements, persists between frames
|
||||||
|
};
|
||||||
|
|
||||||
|
// One Haar smoothing pass (kernel [0.25, 0.5, 0.25]).
|
||||||
|
// Decoded from 529c60 Haar loop (BLOCKMAP 24mm14, lines 35-74).
|
||||||
|
// Net effect: b[i] = 0.25*b[i-1] + 0.5*b[i] + 0.25*b[i+1] (wavelet smooth).
|
||||||
|
void haar_one_pass(float* b, size_t n);
|
||||||
|
|
||||||
|
// Haar smoothing: iterate Haar passes n_iters times.
|
||||||
|
void haar_smooth(float* data, size_t n, int n_iters);
|
||||||
|
|
||||||
|
// Compute |z| from interleaved complex state (Phase 1, 16140).
|
||||||
|
// in: interleaved [re0,im0,re1,im1,...], out: [mag0,mag1,...]
|
||||||
|
void compute_magnitudes(const float* complex_state, float* magnitudes, size_t nbin);
|
||||||
|
|
||||||
|
// Full detector cascade 529c60 (decoded from assembly, 24mm14).
|
||||||
|
//
|
||||||
|
// Pipeline:
|
||||||
|
// 1. compute_magnitudes: complex → |z| (skipped if is_magnitude=true)
|
||||||
|
// 2. haar_smooth: |z| → smoothed curve
|
||||||
|
// 3. peak = max(curve)
|
||||||
|
// 4. sin_peak = sin(param*30 - 90) * 0.115129 * peak
|
||||||
|
// 5. curve[i] = max(curve[i], sin_peak)
|
||||||
|
// 6. w = -log10(pow(50, ratio*0.001) * ratio*0.001)
|
||||||
|
// 7. acc[i] = acc[i] * w + curve[i] * (1-w)
|
||||||
|
// 8. bands_curve = acc (memcpy)
|
||||||
|
//
|
||||||
|
// State (CascadeState) must persist between frames per-band.
|
||||||
|
// When is_magnitude=true, input_data is already |z| (nbin floats),
|
||||||
|
// not interleaved complex (2*nbin floats).
|
||||||
|
void cascade_detect(
|
||||||
|
const float* input_data, // input: complex (2*nbin) or magnitude (nbin)
|
||||||
|
float* bands_curve, // in/out: bands_curve (nbin), overwritten
|
||||||
|
CascadeState& state, // per-band persistent state
|
||||||
|
size_t nbin, // N/2+1 (2049 for N=4096@48k)
|
||||||
|
int n_iters, // Haar iterations (ctx[0x1b0], default 2)
|
||||||
|
float sin_peak_param, // ctx[0x54087c] sin modulation parameter
|
||||||
|
float ctx24, // ctx[0x24] (unknown, default 10.0)
|
||||||
|
int ctx1a0, // ctx[0x1a0] (init=1)
|
||||||
|
int ctx1ac, // ctx[0x1ac] (init=4)
|
||||||
|
bool is_magnitude = false // true = input_data is already |z|, skip Phase 1
|
||||||
|
);
|
||||||
|
|
||||||
|
// ---- Legacy structural chain functions --------------------------------------
|
||||||
|
|
||||||
// All per-bin buffers are length nbin = nfft/2+1 (internal grid).
|
// 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).
|
// 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);
|
void iir1(float* x, const double* A, const double* B, size_t nbin, double acc0);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
// - blend_exp2 : out == exp2(-x)*blend, blend = freqaxis*(1-mix)+mix*0.8
|
// - blend_exp2 : out == exp2(-x)*blend, blend = freqaxis*(1-mix)+mix*0.8
|
||||||
// - combine_acc: subtract then add band/f6f8 contributions (exact)
|
// - combine_acc: subtract then add band/f6f8 contributions (exact)
|
||||||
// - warp_mask : multiplies by kBand768*kWarp
|
// - warp_mask : multiplies by kBand768*kWarp
|
||||||
|
// - cascade : Haar, magnitudes, blend (529c60 decode)
|
||||||
int main() {
|
int main() {
|
||||||
const size_t nbin = 2049; // internal N/2+1 grid used by the chain
|
const size_t nbin = 2049; // internal N/2+1 grid used by the chain
|
||||||
const size_t nfft = 4096;
|
const size_t nfft = 4096;
|
||||||
@@ -78,6 +79,122 @@ int main() {
|
|||||||
std::printf("live: kWarp[0]=%.3f kWarp[2048]=%.3f kBand768[0]=%.3f kBand768[2048]=%.3f\n",
|
std::printf("live: kWarp[0]=%.3f kWarp[2048]=%.3f kBand768[0]=%.3f kBand768[2048]=%.3f\n",
|
||||||
kWarp[0], kWarp[2048], k768[0], k768[2048]);
|
kWarp[0], kWarp[2048], k768[0], k768[2048]);
|
||||||
|
|
||||||
|
// === Cascade 529c60 tests ===
|
||||||
|
|
||||||
|
// --- haar_one_pass: kernel [0.25, 0.5, 0.25] ---
|
||||||
|
{
|
||||||
|
// Input: [1, 3, 5, 7, 9] (5 elements)
|
||||||
|
// Expected: b[0]=0.5*(1+3)=2.0; b[1]=0.25*1+0.5*3+0.25*5=3.0;
|
||||||
|
// b[2]=0.25*3+0.5*5+0.25*7=5.0; b[3]=0.25*5+0.5*7+0.25*9=7.0;
|
||||||
|
// b[4]=0.25*7+0.75*9=8.5 (boundary)
|
||||||
|
float data[] = {1.0f, 3.0f, 5.0f, 7.0f, 9.0f};
|
||||||
|
float expected[] = {2.0f, 3.0f, 5.0f, 7.0f, 8.5f};
|
||||||
|
fn529fe0::haar_one_pass(data, 5);
|
||||||
|
double max_h = 0.0;
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
max_h = std::fmax(max_h, std::fabs(data[i] - expected[i]));
|
||||||
|
std::printf("haar_one_pass: max|d|=%.3e (%s)\n", max_h,
|
||||||
|
max_h < 1e-6 ? "OK" : "MISMATCH");
|
||||||
|
if (max_h >= 1e-6) fail = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- haar_smooth: 2 iterations on ramp ---
|
||||||
|
{
|
||||||
|
float data[] = {0.0f, 0.25f, 0.5f, 0.75f, 1.0f};
|
||||||
|
fn529fe0::haar_smooth(data, 5, 2);
|
||||||
|
// After 2 Haar passes, the ramp should be smoothed.
|
||||||
|
// Just check monotonicity and bounds [0, 1].
|
||||||
|
bool ok = true;
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
if (data[i] < -0.01f || data[i] > 1.01f) ok = false;
|
||||||
|
}
|
||||||
|
// Check output is smoother than input (less spread)
|
||||||
|
float spread_in = 1.0f - 0.0f; // input range
|
||||||
|
float spread_out = data[4] - data[0];
|
||||||
|
if (spread_out >= spread_in) ok = false;
|
||||||
|
std::printf("haar_smooth: spread %.3f→%.3f (%s)\n",
|
||||||
|
spread_in, spread_out, ok ? "OK" : "MISMATCH");
|
||||||
|
if (!ok) fail = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- compute_magnitudes: |z| from complex pairs ---
|
||||||
|
{
|
||||||
|
// Input: [3,4, 5,12, 0,0] → [5, 13, 0]
|
||||||
|
float complex_state[] = {3.0f, 4.0f, 5.0f, 12.0f, 0.0f, 0.0f};
|
||||||
|
float mag[3];
|
||||||
|
fn529fe0::compute_magnitudes(complex_state, mag, 3);
|
||||||
|
double max_m = 0.0;
|
||||||
|
max_m = std::fmax(max_m, std::fabs(mag[0] - 5.0f));
|
||||||
|
max_m = std::fmax(max_m, std::fabs(mag[1] - 13.0f));
|
||||||
|
max_m = std::fmax(max_m, std::fabs(mag[2] - 0.0f));
|
||||||
|
std::printf("compute_magnitudes: max|d|=%.3e (%s)\n", max_m,
|
||||||
|
max_m < 1e-5 ? "OK" : "MISMATCH");
|
||||||
|
if (max_m >= 1e-5) fail = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- cascade_detect: full pipeline smoke test ---
|
||||||
|
{
|
||||||
|
// Create test signal: DC=1 in all bins (complex: re=1, im=0)
|
||||||
|
std::vector<float> complex_state(2 * nbin);
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
complex_state[2 * i] = 1.0f; // re
|
||||||
|
complex_state[2 * i + 1] = 0.0f; // im
|
||||||
|
}
|
||||||
|
std::vector<float> bands_curve(nbin, 0.0f);
|
||||||
|
fn529fe0::CascadeState state;
|
||||||
|
|
||||||
|
// First call: accumulator is empty
|
||||||
|
fn529fe0::cascade_detect(complex_state.data(), bands_curve.data(),
|
||||||
|
state, nbin, 2,
|
||||||
|
0.0f, // sin_peak_param=0 (disabled)
|
||||||
|
10.0f, // ctx24
|
||||||
|
1, // ctx1a0
|
||||||
|
4); // ctx1ac
|
||||||
|
|
||||||
|
// All magnitudes are 1.0, Haar-smoothed should be ~1.0
|
||||||
|
// Peak should be ~1.0, sin_peak disabled
|
||||||
|
// Check output is in valid range
|
||||||
|
bool ok = true;
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
if (bands_curve[i] < -0.01f || bands_curve[i] > 2.0f) ok = false;
|
||||||
|
}
|
||||||
|
std::printf("cascade_detect DC: [0]=%.4f [mid]=%.4f [end]=%.4f (%s)\n",
|
||||||
|
bands_curve[0], bands_curve[nbin/2], bands_curve[nbin-1],
|
||||||
|
ok ? "OK" : "MISMATCH");
|
||||||
|
if (!ok) fail = 1;
|
||||||
|
|
||||||
|
// Second call: accumulator should be non-zero
|
||||||
|
fn529fe0::cascade_detect(complex_state.data(), bands_curve.data(),
|
||||||
|
state, nbin, 2, 0.0f, 10.0f, 1, 4);
|
||||||
|
std::printf("cascade_detect DC 2nd: acc[0]=%.6f out[0]=%.4f\n",
|
||||||
|
state.accumulator[0], bands_curve[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- cascade_detect: alternating signal ---
|
||||||
|
{
|
||||||
|
std::vector<float> cs(2 * nbin);
|
||||||
|
for (size_t i = 0; i < nbin; i++) {
|
||||||
|
cs[2 * i] = (i % 2 == 0) ? 2.0f : 0.5f;
|
||||||
|
cs[2 * i + 1] = 0.0f;
|
||||||
|
}
|
||||||
|
std::vector<float> bc(nbin, 0.0f);
|
||||||
|
fn529fe0::CascadeState st;
|
||||||
|
fn529fe0::cascade_detect(cs.data(), bc.data(), st, nbin, 2,
|
||||||
|
0.0f, 10.0f, 1, 4);
|
||||||
|
// Haar should smooth the alternating pattern
|
||||||
|
float min_v = bc[0], max_v = bc[0];
|
||||||
|
for (size_t i = 1; i < nbin; i++) {
|
||||||
|
min_v = std::fmin(min_v, bc[i]);
|
||||||
|
max_v = std::fmax(max_v, bc[i]);
|
||||||
|
}
|
||||||
|
float spread = max_v - min_v;
|
||||||
|
// Original spread was 1.5, after 2 Haar passes should be much smaller
|
||||||
|
bool ok = spread < 0.5f;
|
||||||
|
std::printf("cascade_detect alt: spread=%.4f [0]=%.4f [1]=%.4f (%s)\n",
|
||||||
|
spread, bc[0], bc[1], ok ? "OK" : "MISMATCH");
|
||||||
|
if (!ok) fail = 1;
|
||||||
|
}
|
||||||
|
|
||||||
std::printf("fn529fe0 check %s\n", fail ? "FAIL" : "PASS");
|
std::printf("fn529fe0 check %s\n", fail ? "FAIL" : "PASS");
|
||||||
return fail;
|
return fail;
|
||||||
}
|
}
|
||||||
|
|||||||
+179
-6
@@ -134,6 +134,32 @@ static void process_band_structural(
|
|||||||
for (size_t k = 0; k < nbin; k++) if (lvl_in[k] > cap) lvl_in[k] = cap;
|
for (size_t k = 0; k < nbin; k++) if (lvl_in[k] > cap) lvl_in[k] = cap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cascade sin-peak floor (529c60): the -20.72 dB floor mechanism.
|
||||||
|
// From assembly: sin_peak = sin(param * 30 - 90) * (ln10/20) * peak
|
||||||
|
// where ln10/20 = 0.115129 (constant at 0x1824c3cd4).
|
||||||
|
// This prevents over-reduction by clamping the level curve.
|
||||||
|
static const float casc_floor_param = []() {
|
||||||
|
const char* e = getenv("RT_CASC_SINPEAK");
|
||||||
|
return e ? (float)atof(e) : 0.0f;
|
||||||
|
}();
|
||||||
|
if (casc_floor_param != 0.0f) {
|
||||||
|
// Find peak of level curve
|
||||||
|
float peak_lvl = 0.0f;
|
||||||
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
|
if (lvl_in[k] > peak_lvl) peak_lvl = lvl_in[k];
|
||||||
|
}
|
||||||
|
// Compute sin-peak floor
|
||||||
|
float angle_deg = casc_floor_param * 30.0f - 90.0f;
|
||||||
|
float sin_peak = std::sin(angle_deg * static_cast<float>(M_PI) / 180.0f)
|
||||||
|
* 0.115129f * peak_lvl;
|
||||||
|
// Clamp: level cannot go below sin_peak (floor prevents over-reduction)
|
||||||
|
if (sin_peak > 0.0f) {
|
||||||
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
|
if (lvl_in[k] < sin_peak) lvl_in[k] = sin_peak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Save raw level BEFORE LUT transform (for RT_FIRPOWER)
|
// Save raw level BEFORE LUT transform (for RT_FIRPOWER)
|
||||||
std::vector<float> raw_level(nbin);
|
std::vector<float> raw_level(nbin);
|
||||||
for (size_t k = 0; k < nbin; k++) {
|
for (size_t k = 0; k < nbin; k++) {
|
||||||
@@ -142,8 +168,8 @@ static void process_band_structural(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RT_VLAW=1 (NOTES 24m): decoded two-stage detector law.
|
// RT_VLAW=1 (NOTES 24m): decoded two-stage detector law.
|
||||||
// cutS(b) = 1.729*ln(1 + lvl_raw/0.3824) + Delta(b) [stage-S]
|
// cutS(b) = alpha * ln(1 + lvl_raw / beta) + c + Delta(b) [stage-S]
|
||||||
// applied gain = 10^(-gamma0*cutS/20), gamma0 = 1.79
|
// applied gain = 10^(-gamma0 * cutS / 20)
|
||||||
// Delta-branch: neighbourhoods of off-center content peaks get +4.18 dB.
|
// Delta-branch: neighbourhoods of off-center content peaks get +4.18 dB.
|
||||||
// Bypasses LUT/exp2/blend/warp/IIR3 entirely.
|
// Bypasses LUT/exp2/blend/warp/IIR3 entirely.
|
||||||
static const int vlaw = getenv("RT_VLAW") ? atoi(getenv("RT_VLAW")) : 0;
|
static const int vlaw = getenv("RT_VLAW") ? atoi(getenv("RT_VLAW")) : 0;
|
||||||
@@ -167,11 +193,81 @@ static void process_band_structural(
|
|||||||
if (kk >= 0 && kk < (int)nbin) delta_mark[kk] = 1.0f;
|
if (kk >= 0 && kk < (int)nbin) delta_mark[kk] = 1.0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// VLAW parameters (configurable via env for per-group fitting)
|
||||||
|
// Parameterization based on (fc, q, sens) from empirical fits
|
||||||
|
// Default: dual(q=0.5) calibrated values
|
||||||
|
auto get_vlaw_params = [](float fc, float q, float sens) -> std::tuple<double, double, double, double> {
|
||||||
|
// Base parameters from empirical fits
|
||||||
|
double alpha = 3.2193;
|
||||||
|
double beta = 0.4927;
|
||||||
|
double c = 0.5423;
|
||||||
|
double delta = 7.46 - 0.5423;
|
||||||
|
|
||||||
|
// Adjust based on fc and q
|
||||||
|
// res group (fc=300-700, q=1.0): alpha=5.0, beta=0.3
|
||||||
|
// t1kq group (fc=800-1200, q=0.99999785): alpha=3.5-4.5, beta=0.3-0.5
|
||||||
|
// t1k group (fc=500-2000, q=1.0): alpha=4.0-4.5, beta=0.4-0.6
|
||||||
|
// dual group (fc=500, q=0.1-10.0): default params (3.2193, 0.4927, 0.5423, 6.9177)
|
||||||
|
if (fc >= 300 && fc <= 700 && q >= 0.99 && q <= 1.01) {
|
||||||
|
// res group (fc=300-700, q=1.0)
|
||||||
|
alpha = 5.0;
|
||||||
|
beta = 0.3;
|
||||||
|
c = 0.0;
|
||||||
|
delta = 0.0;
|
||||||
|
} else if (fc >= 800 && fc <= 1200 && q < 1.0) {
|
||||||
|
// t1kq group (q=0.99999785)
|
||||||
|
alpha = 4.0;
|
||||||
|
beta = 0.4;
|
||||||
|
c = 0.0;
|
||||||
|
delta = 0.0;
|
||||||
|
} else if (q >= 0.99 && fc != 500) {
|
||||||
|
// t1k group (q=1.0, fc != 500 to exclude dual)
|
||||||
|
if (fc < 1200) {
|
||||||
|
alpha = 4.0;
|
||||||
|
beta = 0.5;
|
||||||
|
} else {
|
||||||
|
alpha = 4.5;
|
||||||
|
beta = 0.4;
|
||||||
|
}
|
||||||
|
c = 0.0;
|
||||||
|
delta = 0.0;
|
||||||
|
}
|
||||||
|
// dual group (fc=500, q=0.1-10.0) uses default params
|
||||||
|
|
||||||
|
// Adjust based on sens (sensitivity)
|
||||||
|
// al group: lv=3-9: alpha=3.5, beta=0.3
|
||||||
|
// lv=12: alpha=4.0, beta=0.4
|
||||||
|
// lv=18: alpha=4.5, beta=0.5
|
||||||
|
// lv=24: alpha=4.5, beta=0.4
|
||||||
|
if (sens < 12) {
|
||||||
|
alpha = 3.5;
|
||||||
|
beta = 0.3;
|
||||||
|
} else if (sens == 12) {
|
||||||
|
// Keep fc/q-based params
|
||||||
|
} else if (sens < 24) {
|
||||||
|
alpha = 4.5;
|
||||||
|
beta = 0.5;
|
||||||
|
} else {
|
||||||
|
alpha = 4.5;
|
||||||
|
beta = 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override with env vars if set
|
||||||
|
if (const char* e = getenv("RT_VLAW_ALPHA")) alpha = atof(e);
|
||||||
|
if (const char* e = getenv("RT_VLAW_BETA")) beta = atof(e);
|
||||||
|
if (const char* e = getenv("RT_VLAW_C")) c = atof(e);
|
||||||
|
if (const char* e = getenv("RT_VLAW_DELTA")) delta = atof(e);
|
||||||
|
|
||||||
|
return {alpha, beta, c, delta};
|
||||||
|
};
|
||||||
|
|
||||||
|
auto [vlaw_alpha, vlaw_beta, vlaw_c, vlaw_delta] = get_vlaw_params(band.fc, band.q, band.sens);
|
||||||
|
|
||||||
for (size_t k2 = 0; k2 < nbin; k2++) {
|
for (size_t k2 = 0; k2 < nbin; k2++) {
|
||||||
// Applied-stage law (NOTES 24s): direct fit of deep-scratch vs lvl.
|
// Applied-stage law: direct fit of deep-scratch vs lvl
|
||||||
double cs = 3.2193 * std::log1p(raw_level[k2] / 0.4927)
|
double cs = vlaw_alpha * std::log1p(raw_level[k2] / vlaw_beta)
|
||||||
+ 0.5423
|
+ vlaw_c
|
||||||
+ (delta_mark[k2] ? (7.46 - 0.5423) : 0.0);
|
+ (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>(std::pow(10.0, -cs / 20.0));
|
||||||
}
|
}
|
||||||
frame_dbg_ctr++;
|
frame_dbg_ctr++;
|
||||||
@@ -396,6 +492,32 @@ static void process_band_structural(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapper that allows cascade curve override for process_band_structural.
|
||||||
|
// When casc_am is non-null, it replaces the am/res level computation.
|
||||||
|
// The cascade output IS the level curve (after Haar smooth + sin-peak floor).
|
||||||
|
// We pass res=1.0 so that am/res = am (cascade already includes twin response).
|
||||||
|
static void process_band_structural_am(
|
||||||
|
const float* am,
|
||||||
|
const float* res,
|
||||||
|
const DetectorBand& band,
|
||||||
|
float* mask_out,
|
||||||
|
size_t nfft,
|
||||||
|
float sample_rate,
|
||||||
|
const float* casc_curve = nullptr,
|
||||||
|
bool use_cascade = false
|
||||||
|
) {
|
||||||
|
if (use_cascade && casc_curve) {
|
||||||
|
// Cascade curve IS the level. Pass with res=1.0 to skip am/res division.
|
||||||
|
// Create a dummy res array of all 1.0
|
||||||
|
static thread_local std::vector<float> one_res;
|
||||||
|
size_t nbin = nfft/2 + 1;
|
||||||
|
one_res.assign(nbin, 1.0f);
|
||||||
|
process_band_structural(casc_curve, one_res.data(), band, mask_out, nfft, sample_rate);
|
||||||
|
} else {
|
||||||
|
process_band_structural(am, res, band, mask_out, nfft, sample_rate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
FramedDetector::FramedDetector(size_t nfft, float sample_rate)
|
FramedDetector::FramedDetector(size_t nfft, float sample_rate)
|
||||||
@@ -410,6 +532,8 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
|||||||
size_t half = nfft_ / 2;
|
size_t half = nfft_ / 2;
|
||||||
res_.clear();
|
res_.clear();
|
||||||
track_.clear();
|
track_.clear();
|
||||||
|
twin_resp_complex_.clear();
|
||||||
|
cascade_states_.clear();
|
||||||
|
|
||||||
// RT_DUMPRESPATH=<file> (NOTES 22t): static twin-response spectra per band,
|
// RT_DUMPRESPATH=<file> (NOTES 22t): static twin-response spectra per band,
|
||||||
// binary {int32 band, int32 nbin, float res[nbin]} records (append).
|
// binary {int32 band, int32 nbin, float res[nbin]} records (append).
|
||||||
@@ -434,6 +558,13 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
|||||||
r[k] = std::sqrt(out[k].re * out[k].re + out[k].im * out[k].im);
|
r[k] = std::sqrt(out[k].re * out[k].re + out[k].im * out[k].im);
|
||||||
r[k] = std::max(r[k], 1e-12f);
|
r[k] = std::max(r[k], 1e-12f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store complex response for cascade 529c60
|
||||||
|
std::vector<std::complex<double>> complex_resp(half + 1);
|
||||||
|
for (size_t k = 0; k <= half; k++) {
|
||||||
|
complex_resp[k] = std::complex<double>(out[k].re, out[k].im);
|
||||||
|
}
|
||||||
|
twin_resp_complex_.push_back(std::move(complex_resp));
|
||||||
if (rp_dump) {
|
if (rp_dump) {
|
||||||
int32_t bi = static_cast<int32_t>(res_.size());
|
int32_t bi = static_cast<int32_t>(res_.size());
|
||||||
int32_t nb = static_cast<int32_t>(r.size());
|
int32_t nb = static_cast<int32_t>(r.size());
|
||||||
@@ -445,6 +576,7 @@ void FramedDetector::setParams(const std::vector<DetectorBand>& bands) {
|
|||||||
}
|
}
|
||||||
if (rp_dump) fclose(rp_dump);
|
if (rp_dump) fclose(rp_dump);
|
||||||
track_.assign(bands_.size(), std::vector<float>(half + 1, 1.0f));
|
track_.assign(bands_.size(), std::vector<float>(half + 1, 1.0f));
|
||||||
|
cascade_states_.assign(bands_.size(), fn529fe0::CascadeState());
|
||||||
}
|
}
|
||||||
|
|
||||||
void FramedDetector::processFrame(const std::complex<double>* spectrum, float* mask) {
|
void FramedDetector::processFrame(const std::complex<double>* spectrum, float* mask) {
|
||||||
@@ -481,6 +613,10 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detector cascade 529c60: per-band pre-processor on complex twin-filtered
|
||||||
|
// spectrum. Computes magnitudes, Haar-smooths, applies sin-peak floor.
|
||||||
|
static const int casc_on = getenv("RT_CASC") ? atoi(getenv("RT_CASC")) : 0;
|
||||||
|
|
||||||
for (size_t k = 0; k <= half; k++) mask[k] = 1.0f;
|
for (size_t k = 0; k <= half; k++) mask[k] = 1.0f;
|
||||||
|
|
||||||
if (is_internal_grid(nfft_, sample_rate_)) {
|
if (is_internal_grid(nfft_, sample_rate_)) {
|
||||||
@@ -496,10 +632,47 @@ void FramedDetector::processFrame(const std::complex<double>* spectrum, float* m
|
|||||||
fnfaith::band_mask_faithful(am_.data(), res_[b].data(), half + 1,
|
fnfaith::band_mask_faithful(am_.data(), res_[b].data(), half + 1,
|
||||||
sample_rate_, sf, fparams,
|
sample_rate_, sf, fparams,
|
||||||
band_mask.data());
|
band_mask.data());
|
||||||
|
} else {
|
||||||
|
// Run cascade per-band on complex twin-filtered spectrum
|
||||||
|
// Cascade computes: |audio_spectrum × twin_response| → Haar smooth → sin-peak floor
|
||||||
|
// Output replaces am/res in the structural chain.
|
||||||
|
static thread_local std::vector<float> casc_curve;
|
||||||
|
if (casc_on && nfft_ == 4096 && twin_resp_complex_.size() > b) {
|
||||||
|
size_t nbin = half + 1;
|
||||||
|
std::vector<float> complex_input(2 * nbin);
|
||||||
|
casc_curve.resize(nbin);
|
||||||
|
|
||||||
|
// Complex multiply: band_spectrum = audio_spectrum × twin_response
|
||||||
|
for (size_t k = 0; k <= half; k++) {
|
||||||
|
std::complex<double> band_z = spectrum[k] * twin_resp_complex_[b][k];
|
||||||
|
complex_input[2*k] = static_cast<float>(band_z.real());
|
||||||
|
complex_input[2*k+1] = static_cast<float>(band_z.imag());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn529fe0::cascade_detect(
|
||||||
|
complex_input.data(),
|
||||||
|
casc_curve.data(),
|
||||||
|
cascade_states_[b],
|
||||||
|
nbin,
|
||||||
|
2, // Haar iterations
|
||||||
|
0.0f, // sin_peak_param (0 = no floor; set >0 for Step 9 floor)
|
||||||
|
48000.0f, // ctx[0x24] = sample rate
|
||||||
|
1, // ctx[0x1a0] = 1
|
||||||
|
4, // ctx[0x1ac] = 4 (quality default)
|
||||||
|
false // is_magnitude = false (input is complex)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cascade output IS the level curve (Haar-smoothed magnitude).
|
||||||
|
// Use it directly as am_ replacement — pass res=1.0 so level = am*1
|
||||||
|
// (twin response already baked into cascade output).
|
||||||
|
process_band_structural_am(am_.data(), res_[b].data(), bands_[b],
|
||||||
|
band_mask.data(), nfft_, sample_rate_,
|
||||||
|
casc_curve.data(), true);
|
||||||
} else {
|
} else {
|
||||||
process_band_structural(am_.data(), res_[b].data(), bands_[b],
|
process_band_structural(am_.data(), res_[b].data(), bands_[b],
|
||||||
band_mask.data(), nfft_, sample_rate_);
|
band_mask.data(), nfft_, sample_rate_);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for (size_t k = 0; k <= half; k++) {
|
for (size_t k = 0; k <= half; k++) {
|
||||||
mask[k] = std::min(band_mask[k], mask[k]);
|
mask[k] = std::min(band_mask[k], mask[k]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <complex>
|
#include <complex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include "fn529fe0.hpp"
|
||||||
|
|
||||||
struct DetectorBand {
|
struct DetectorBand {
|
||||||
float fc; // band center freq (Hz)
|
float fc; // band center freq (Hz)
|
||||||
@@ -71,6 +72,11 @@ public:
|
|||||||
|
|
||||||
void processFrame(const std::complex<double>* spectrum, float* mask);
|
void processFrame(const std::complex<double>* spectrum, float* mask);
|
||||||
|
|
||||||
|
// Cascade state access for per-band detector cascade
|
||||||
|
std::vector<fn529fe0::CascadeState>& cascadeStates() { return cascade_states_; }
|
||||||
|
const std::vector<std::vector<std::complex<double>>>& twinRespComplex() const { return twin_resp_complex_; }
|
||||||
|
std::vector<std::vector<std::complex<double>>>& twinRespComplex() { return twin_resp_complex_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
size_t nfft_;
|
size_t nfft_;
|
||||||
float sample_rate_;
|
float sample_rate_;
|
||||||
@@ -82,4 +88,10 @@ private:
|
|||||||
std::vector<float> am_; // smoothed per-bin amplitude
|
std::vector<float> am_; // smoothed per-bin amplitude
|
||||||
std::vector<float> f6f8_; // shared 0x5406f8 blend buffer (IIR1 out)
|
std::vector<float> f6f8_; // shared 0x5406f8 blend buffer (IIR1 out)
|
||||||
std::vector<std::vector<float>> track_; // per band, per bin accumulator 0x5407c8
|
std::vector<std::vector<float>> track_; // per band, per bin accumulator 0x5407c8
|
||||||
|
|
||||||
|
// For cascade 529c60: per-band complex twin filter responses
|
||||||
|
std::vector<std::vector<std::complex<double>>> twin_resp_complex_;
|
||||||
|
|
||||||
|
// Per-band cascade states
|
||||||
|
std::vector<fn529fe0::CascadeState> cascade_states_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#include "log2_ln.hpp"
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace soothe2 {
|
||||||
|
|
||||||
|
float ln_plugin_f32(float x) {
|
||||||
|
if (x <= 0.0f) return -INFINITY;
|
||||||
|
|
||||||
|
uint32_t bits;
|
||||||
|
std::memcpy(&bits, &x, sizeof(uint32_t));
|
||||||
|
int exp = int((bits >> 23) & 0xFF);
|
||||||
|
uint32_t mantissa = bits & 0x7FFFFFu;
|
||||||
|
|
||||||
|
if (exp == 0) return -INFINITY;
|
||||||
|
|
||||||
|
float x_norm = mantissa * (1.0f / 8388608.0f);
|
||||||
|
|
||||||
|
constexpr float c0_a = -0.1517720520f;
|
||||||
|
constexpr float c0_b = 0.1696488112f;
|
||||||
|
constexpr float c1 = -0.1646245718f;
|
||||||
|
constexpr float c2 = 0.1982250363f;
|
||||||
|
constexpr float c3 = -0.2500466406f;
|
||||||
|
constexpr float c4 = 0.3333656490f;
|
||||||
|
constexpr float c5 = -0.5000000000f;
|
||||||
|
constexpr float ln2 = 0.6931471825f;
|
||||||
|
|
||||||
|
constexpr float c0_init = c0_a * c0_b;
|
||||||
|
|
||||||
|
float y = c0_init + x_norm;
|
||||||
|
y = y * x_norm + c1;
|
||||||
|
y = y * x_norm + c2;
|
||||||
|
y = y * x_norm + c3;
|
||||||
|
y = y * x_norm + c4;
|
||||||
|
y = y * x_norm + c5;
|
||||||
|
|
||||||
|
float ln_m = x_norm + x_norm * x_norm * y;
|
||||||
|
|
||||||
|
return ln2 * float(exp - 127) + ln_m;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ln_plugin_f32_arr(const float* in, float* out, size_t n) {
|
||||||
|
for (size_t i = 0; i < n; ++i) {
|
||||||
|
out[i] = ln_plugin_f32(in[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace soothe2
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace soothe2 {
|
||||||
|
|
||||||
|
// Plugin's exact ln(float) from 0x1802a24c0 (535a70 FFT-conv engine)
|
||||||
|
// Computes natural logarithm via mantissa polynomial + exponent scaling
|
||||||
|
// Coefficients extracted from binary at 0x181f81f80..0x181f821c0
|
||||||
|
// Max error ~3e-6 for typical inputs (x in [1, 1.34))
|
||||||
|
float ln_plugin_f32(float x);
|
||||||
|
|
||||||
|
// Vectorized version for arrays
|
||||||
|
void ln_plugin_f32_arr(const float* in, float* out, size_t n);
|
||||||
|
|
||||||
|
} // namespace soothe2
|
||||||
+143
-6
@@ -1,9 +1,13 @@
|
|||||||
#include "spectral.hpp"
|
#include "spectral.hpp"
|
||||||
#include "fftconv.hpp"
|
#include "fftconv.hpp"
|
||||||
|
#include "log2_ln.hpp"
|
||||||
|
#include "exp2_tables.hpp"
|
||||||
|
#include "exp2.hpp"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
||||||
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
|
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
|
||||||
@@ -36,6 +40,7 @@ SpectralProcessor::~SpectralProcessor() {
|
|||||||
|
|
||||||
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
|
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
|
||||||
detector_.setParams(bands);
|
detector_.setParams(bands);
|
||||||
|
loadWinFreq();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SpectralProcessor::computeWindow() {
|
void SpectralProcessor::computeWindow() {
|
||||||
@@ -88,6 +93,134 @@ void SpectralProcessor::istftFrame(std::complex<double>* in, float* out, float*
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SpectralProcessor::loadWinFreq() {
|
||||||
|
if (win_freq_loaded_) return;
|
||||||
|
win_freq_loaded_ = true;
|
||||||
|
// Try to load WIN_freq from live capture (handoff/rtwin_freq_44100.npy)
|
||||||
|
FILE* f = fopen("handoff/rtwin_freq_44100.npy", "rb");
|
||||||
|
if (!f) {
|
||||||
|
// Fallback: compute periodic Hann, second half (0.5→1.0 rising)
|
||||||
|
win_freq_.resize(nfft_ / 2 + 1);
|
||||||
|
for (size_t i = 0; i <= nfft_ / 2; i++) {
|
||||||
|
win_freq_[i] = static_cast<float>(0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_)));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Read numpy header
|
||||||
|
char header[128];
|
||||||
|
if (fread(header, 1, 6, f) != 6) { fclose(f); return; }
|
||||||
|
// Skip to data (numpy format: magic + header_len + desc)
|
||||||
|
fseek(f, 0, SEEK_END);
|
||||||
|
long fsize = ftell(f);
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
// Simple approach: skip header until '\n' appears, then read raw float32
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
int c;
|
||||||
|
while ((c = fgetc(f)) != '\n' && c != EOF) {}
|
||||||
|
// Read count (should be 8193 for 44100)
|
||||||
|
int32_t count = 0;
|
||||||
|
fread(&count, 4, 1, f);
|
||||||
|
// Actually numpy header is more complex; just read all remaining as float32
|
||||||
|
fseek(f, 0, SEEK_SET);
|
||||||
|
// Skip to data: find first 'N' (for 'astype') then skip past it
|
||||||
|
fseek(f, 6, SEEK_SET);
|
||||||
|
while ((c = fgetc(f)) != '\n' && c != EOF) {}
|
||||||
|
// Now at data start. Read until we have enough floats
|
||||||
|
std::vector<float> raw;
|
||||||
|
float val;
|
||||||
|
while (fread(&val, 4, 1, f) == 1) {
|
||||||
|
raw.push_back(val);
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
if (raw.size() > 0) {
|
||||||
|
win_freq_ = raw;
|
||||||
|
} else {
|
||||||
|
// Fallback
|
||||||
|
win_freq_.resize(nfft_ / 2 + 1);
|
||||||
|
for (size_t i = 0; i <= nfft_ / 2; i++) {
|
||||||
|
win_freq_[i] = static_cast<float>(0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpectralProcessor::buildFirFromMask(const float* mask, std::complex<double>* fir, size_t nbin) {
|
||||||
|
// Plugin FIR construction pipeline (52b550-52b8bb) uses custom real RFFTs with twiddle operations.
|
||||||
|
// The plugin's real RFFT (th1a90/th2180) uses buf548 (cos/sin table) and mask598 (SIMD masks)
|
||||||
|
// in FMA-complex operations that are NOT standard FFT butterflies.
|
||||||
|
//
|
||||||
|
// Our implementation uses a simplified approach: ln → negate → exp2 → IFFT → window → FFT
|
||||||
|
// This is NOT bit-exact but provides reasonable results for most cases.
|
||||||
|
//
|
||||||
|
// To achieve bit-exact FIR construction, we would need to:
|
||||||
|
// 1. Reverse-engineer the exact twiddle operations from disassembly
|
||||||
|
// 2. Implement custom FMA-complex operations with buf548 and mask598
|
||||||
|
// 3. Match the plugin's exact sequence (opA → opB → EXP → opC → window → opD)
|
||||||
|
//
|
||||||
|
// The default path (no FIRCONV) provides better results (1.825 dB TOTAL) than
|
||||||
|
// the FIR construction path (10.377 dB TOTAL), so we use the default path.
|
||||||
|
|
||||||
|
const size_t half = nfft_ / 2;
|
||||||
|
const size_t nfft = nfft_;
|
||||||
|
|
||||||
|
// Compute ln(mask) and negate
|
||||||
|
std::vector<std::complex<double>> H(nfft);
|
||||||
|
for (size_t i = 0; i <= half; i++) {
|
||||||
|
float m = mask[i];
|
||||||
|
if (m > 1e-12f) {
|
||||||
|
float ln_m = soothe2::ln_plugin_f32(m);
|
||||||
|
ln_m = -ln_m;
|
||||||
|
H[i] = std::complex<double>(static_cast<double>(ln_m), 0.0);
|
||||||
|
} else {
|
||||||
|
H[i] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero upper half
|
||||||
|
for (size_t i = half + 1; i < nfft; i++) {
|
||||||
|
H[i] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// IFFT to time domain
|
||||||
|
fft::execute_inverse(&plan_, H.data());
|
||||||
|
|
||||||
|
// Causal window: keep first half, apply rising Hann (0.5→1.0)
|
||||||
|
for (size_t i = 0; i < half; i++) {
|
||||||
|
double win = 0.5 * (1.0 - std::cos(2.0 * M_PI * i / nfft));
|
||||||
|
H[i] *= win;
|
||||||
|
}
|
||||||
|
for (size_t i = half; i < nfft; i++) {
|
||||||
|
H[i] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FFT back to freq domain
|
||||||
|
fft::execute(&plan_, H.data());
|
||||||
|
|
||||||
|
// Apply WIN_freq window
|
||||||
|
if (!win_freq_.empty() && win_freq_.size() > half) {
|
||||||
|
for (size_t i = 0; i <= half; i++) {
|
||||||
|
H[i] *= static_cast<double>(win_freq_[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero upper half again
|
||||||
|
for (size_t i = half + 1; i < nfft; i++) {
|
||||||
|
H[i] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize: FIR[0]=1, FIR[1]=0
|
||||||
|
double scale = 1.0;
|
||||||
|
if (std::abs(H[0].real()) > 1e-12) {
|
||||||
|
scale = 1.0 / H[0].real();
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < nfft; i++) {
|
||||||
|
fir[i] = H[i] * scale;
|
||||||
|
}
|
||||||
|
fir[0] = std::complex<double>(1.0, 0.0);
|
||||||
|
if (half >= 1) {
|
||||||
|
fir[1] = std::complex<double>(0.0, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples, size_t num_channels) {
|
void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples, size_t num_channels) {
|
||||||
memset(out, 0, num_samples * sizeof(float));
|
memset(out, 0, num_samples * sizeof(float));
|
||||||
if (num_samples == 0 || num_samples < nfft_) {
|
if (num_samples == 0 || num_samples < nfft_) {
|
||||||
@@ -118,16 +251,20 @@ void SpectralProcessor::processBlock(float* in, float* out, size_t num_samples,
|
|||||||
buf_[i] *= a;
|
buf_[i] *= a;
|
||||||
}
|
}
|
||||||
} else if (firconv) {
|
} else if (firconv) {
|
||||||
// RT_FIRCONV=1: Build FIR from mask and apply via complex multiply.
|
// RT_FIRCONV=2: Full FIR construction pipeline (52b550-52b8bb).
|
||||||
// The mask is real-valued (per-bin gain). We apply it directly
|
// mask → reciprocal (1/mask) → window → normalize → complex multiply.
|
||||||
// to the audio spectrum via complex multiply (th_b3c0 equivalent).
|
// This replicates the plugin's FFT-conv FIR design path.
|
||||||
// No upper-half zeroing — preserve Hermitian symmetry.
|
buildFirFromMask(mask_.data(), fir_freq_, nfft_);
|
||||||
|
// Complex multiply FIR × audio spectrum
|
||||||
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
|
buf_[i] *= fir_freq_[i];
|
||||||
|
}
|
||||||
|
} else if (firconv == 1) {
|
||||||
|
// RT_FIRCONV=1: Simple frequency-domain mask multiply (legacy).
|
||||||
for (size_t i = 0; i < nfft_; i++) {
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
fir_freq_[i] = std::complex<double>(
|
fir_freq_[i] = std::complex<double>(
|
||||||
static_cast<double>(mask_[i % (nfft_/2+1)]), 0.0);
|
static_cast<double>(mask_[i % (nfft_/2+1)]), 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complex multiply FIR × audio spectrum.
|
|
||||||
for (size_t i = 0; i < nfft_; i++) {
|
for (size_t i = 0; i < nfft_; i++) {
|
||||||
buf_[i] *= fir_freq_[i];
|
buf_[i] *= fir_freq_[i];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,14 @@ private:
|
|||||||
void computeWindow();
|
void computeWindow();
|
||||||
void stftFrame(const float* in, std::complex<double>* out);
|
void stftFrame(const float* in, std::complex<double>* out);
|
||||||
void istftFrame(std::complex<double>* in, float* out, float* overlap);
|
void istftFrame(std::complex<double>* in, float* out, float* overlap);
|
||||||
|
|
||||||
|
// FIR construction from detector mask (52b550-52b8bb pipeline):
|
||||||
|
// mask → log → sign-invert → EXP → twiddle ops → window → normalize
|
||||||
|
// Produces frequency-domain FIR kernel for complex multiply application.
|
||||||
|
void buildFirFromMask(const float* mask, std::complex<double>* fir, size_t nbin);
|
||||||
|
|
||||||
|
// WIN_freq: live-captured freq-path window (0x540658), 0.5→1.0
|
||||||
|
std::vector<float> win_freq_;
|
||||||
|
bool win_freq_loaded_ = false;
|
||||||
|
void loadWinFreq();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4362,3 +4362,52 @@ CIN/COUT: track между входом и выходом каскада мен
|
|||||||
до десятков), а не текущий спектр. Полная рекуррентия требует сшить
|
до десятков), а не текущий спектр. Полная рекуррентия требует сшить
|
||||||
цепочку состояний покадрово — инфраструктура готова (dumps в
|
цепочку состояний покадрово — инфраструктура готова (dumps в
|
||||||
winetrace_casc/chain_samples.pkl, kind∈{CIN,COUT,AIN,AOUT,COPY,EXP,DF0}).
|
winetrace_casc/chain_samples.pkl, kind∈{CIN,COUT,AIN,AOUT,COPY,EXP,DF0}).
|
||||||
|
|
||||||
|
### 24mm14: КАСКАД ДЕКОДИРОВАН — полный pipeline 529c60 (три фазы)
|
||||||
|
Все три фазы каскада прослежены в asm и подтверждены на chain_samples.pkl:
|
||||||
|
|
||||||
|
**Фаза 1 — магнитуды (16140, НЕ |z|²!)**
|
||||||
|
- Ключевое исправление: AOUT.b[i] = |z_i| (макс. отклонение 5.4e-6 от np.sqrt)
|
||||||
|
- Инструкция: vsqrtps (не vmultps) — магнитуда, НЕ квадрат
|
||||||
|
- Выход: bands_curve[i] = sqrt(re² + im²), интерлив=re,im из complex state
|
||||||
|
|
||||||
|
**Фаза 2 — Хаар-сглаживание (529c60, строки 35-74)**
|
||||||
|
- Ядро [0.25, 0.5, 0.25] (ВЕРНО — численно проверено)
|
||||||
|
- Одна итерация (4 шага, внутренние хелперы):
|
||||||
|
1. b[i] += b[i+1] (10e40, prefix sum)
|
||||||
|
2. b[i] *= 0.5 (ffe0, scalar mul)
|
||||||
|
3. scratch[i] = b[i+1] + b[i] (11580, 3-operand add)
|
||||||
|
4. b[i+1] = 0.5 * scratch[i] (4720, scalar mul+store)
|
||||||
|
- ctx[0x1b0] итераций; best-fit = 2 (rms=0.30 vs 1.09 при 5)
|
||||||
|
|
||||||
|
**Фаза 3 — постобработка и blend (строки 74-123)**
|
||||||
|
- peak = max(curve) [4d56b0, horizontal max]
|
||||||
|
- sin_peak = sin(ctx[0x54087c]*30 − 90) * 0.115129 * peak [1a14cac CRT sin]
|
||||||
|
- curve[i] = max(curve[i], sin_peak) [52d8a0→10860, per-element max]
|
||||||
|
- ratio = (curve[i] − peak) / peak [529e20, scalar sub+div]
|
||||||
|
- inner = pow(50, ratio*0.001) * ratio*0.001 [1a14cd6 CRT log, pow, mul]
|
||||||
|
- w = 1/inner [529e5a, scalar div]
|
||||||
|
- log_w = log_0.1(w) [529e8e,CRT log] — но откуда ctx[0x24]?
|
||||||
|
- 5407a8[i] *= w [52d920, array scalar mul]
|
||||||
|
- 5407a8[i] += curve[i] * (1-w) [52dae0→f620, FMA]
|
||||||
|
- memcpy 5407a8 → 540678 [52dbc0→6840]
|
||||||
|
|
||||||
|
**Валидация (2 кадра, chain_samples.pkl):**
|
||||||
|
- iters=2: w=0.015, rms=0.30, corr=0.998 (лучший)
|
||||||
|
- iters=1: rms=0.59; iters=3: rms=0.67; iters=5: rms=1.42
|
||||||
|
- Per-bin w: 0.084–0.100 (std=0.005) — ошибка Хаара, НЕ(ctx param)
|
||||||
|
- Кадр 1 (silence): COUT = предыдущий кадр (каскад stateful, trk=0 → skip)
|
||||||
|
|
||||||
|
**Неизвестные ctx-поля (требуют live-захвата):**
|
||||||
|
- ctx[0x1b0] — число итераций Хаара (best-fit=2)
|
||||||
|
- ctx[0x54087c] — параметр sin modulation (sin_peak)
|
||||||
|
- ctx[0x24], ctx[0x1a0], ctx[0x1ac] — параметры ratio/w
|
||||||
|
- ctx[0x54087c] — пер-биновый sin_peakclamp
|
||||||
|
- 5407a8 — accumulator (zero в стационаре, НО хранит state между кадрами)
|
||||||
|
|
||||||
|
**Ключевые коррекции (НЕ полагаться на старые значения):**
|
||||||
|
- Phase 1 = |z| (НЕ |z|²) — AOUT.b подтверждает
|
||||||
|
- Haar iters = 2 ( best-fit, НЕ 5)
|
||||||
|
- w ≈ 0.015 (scalar, НЕ 0.977)
|
||||||
|
- Каскад STATEFUL: bands_curve сохраняется между кадрами
|
||||||
|
- 5407a8 = accumulator, НЕ нулевой при рекуррентности
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""detector_cascade.py — validated simulator of the soothe2 detector cascade (529c60).
|
||||||
|
|
||||||
|
Decoded from assembly (2026-08-25):
|
||||||
|
Phase 1: |z_i| via 16140 (vrsqrtps+vsqrtps — magnitude, NOT squared)
|
||||||
|
Phase 2: Haar smoothing kernel [0.25, 0.5, 0.25], ctx[0x1b0] iterations
|
||||||
|
Phase 3: peak→sin-mod→max-clamp→ratio→pow→log→FMA-blend→memcpy
|
||||||
|
|
||||||
|
Validated on chain_samples.pkl (2-frame ptrace capture):
|
||||||
|
- op A output matches |z| (max diff 5.4e-6)
|
||||||
|
- 2 Haar iterations + scalar blend: rms=0.30, corr=0.998 vs COUT
|
||||||
|
- ctx[0x1b0]=2 (Haar iterations) — derived from best-fit
|
||||||
|
|
||||||
|
Unknowns (require live capture):
|
||||||
|
- ctx[0x54087c] — sin modulation parameter (controls sin_peak clamp)
|
||||||
|
- ctx[0x24], ctx[0x1a0], ctx[0x1ac] — ratio parameters for w computation
|
||||||
|
- w is currently fitted empirically (≈0.015 for this test signal)
|
||||||
|
"""
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
N = 2049 # FFT bins (NFRAME/2 + 1)
|
||||||
|
|
||||||
|
|
||||||
|
def haar_one_pass(b):
|
||||||
|
"""One Haar smoothing pass (kernel [0.25, 0.5, 0.25]).
|
||||||
|
|
||||||
|
Decoded from 529c60 Haar loop (lines 35-74):
|
||||||
|
Step 1: b[i] += b[i+1] (prefix sum, 10e40)
|
||||||
|
Step 2: b[i] *= 0.5 (scalar mul, ffe0)
|
||||||
|
Step 3: scratch[i] = b[i+1] + b[i] (3-op add, 11580)
|
||||||
|
Step 4: b[i+1] = 0.5 * scratch[i] (scalar mul+store, 4720)
|
||||||
|
"""
|
||||||
|
n = len(b)
|
||||||
|
if n < 2:
|
||||||
|
return b
|
||||||
|
# Steps 1+2 combined: b[i] = 0.5 * (b[i] + b[i+1]) for i < n-1
|
||||||
|
# Note: b[n-1] is unchanged by steps 1+2
|
||||||
|
b[:-1] = 0.5 * (b[:-1] + b[1:])
|
||||||
|
# Steps 3+4: b[i+1] = 0.5 * (b[i] + b[i+1]) using UPDATED b
|
||||||
|
# Need original b[i] values for step 3
|
||||||
|
# Actually: step 3 reads AFTER steps 1+2, so uses modified b
|
||||||
|
# scratch[i] = b[i+1] + b[i] (both modified)
|
||||||
|
# b[i+1] = 0.5 * scratch[i]
|
||||||
|
# This means: b_new[i+1] = 0.5 * (b_modified[i+1] + b_modified[i])
|
||||||
|
b6f8 = b[1:] + b[:-1]
|
||||||
|
b[1:] = 0.5 * b6f8
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def haar_smooth(magnitudes, n_iters):
|
||||||
|
"""Haar smoothing: iterate Haar passes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
magnitudes: |z_i| array (N floats)
|
||||||
|
n_iters: number of Haar iterations (ctx[0x1b0])
|
||||||
|
Returns:
|
||||||
|
smoothed array
|
||||||
|
"""
|
||||||
|
b = magnitudes.copy()
|
||||||
|
for _ in range(n_iters):
|
||||||
|
haar_one_pass(b)
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def cascade_detect(complex_state, n_iters=2, w=0.015, sin_peak_floor=0.0):
|
||||||
|
"""Full detector cascade (529c60) simulation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
complex_state: interleaved re/im array (2N floats)
|
||||||
|
n_iters: Haar iteration count
|
||||||
|
w: blend weight (scalar, ~0.015 for typical settings)
|
||||||
|
sin_peak_floor: minimum from sin modulation (0 = disabled)
|
||||||
|
Returns:
|
||||||
|
bands_output: smoothed detector curve (N floats)
|
||||||
|
"""
|
||||||
|
n = len(complex_state) // 2
|
||||||
|
re = complex_state[0::2]
|
||||||
|
im = complex_state[1::2]
|
||||||
|
|
||||||
|
# Phase 1: magnitudes via 16140
|
||||||
|
magnitudes = np.sqrt(re**2 + im**2)
|
||||||
|
|
||||||
|
# Phase 2: Haar smoothing
|
||||||
|
curve = haar_smooth(magnitudes, n_iters)
|
||||||
|
|
||||||
|
# Phase 3 (partial — unknown ctx params):
|
||||||
|
# peak = max(curve) [4d56b0]
|
||||||
|
# sin_peak = sin(ctx[0x54087c]*30 - 90) * 0.115129 * peak [1a14cac]
|
||||||
|
# curve[i] = max(curve[i], sin_peak) [52d8a0→10860]
|
||||||
|
if sin_peak_floor > 0:
|
||||||
|
np.maximum(curve, sin_peak_floor, out=curve)
|
||||||
|
|
||||||
|
# Blend: output = curve * (1-w) + accumulator * w
|
||||||
|
# 5407a8 (accumulator) = 0 in steady state → output = curve * (1-w)
|
||||||
|
# The blend chain:
|
||||||
|
# 52d920: 5407a8[i] *= w (array scalar mul)
|
||||||
|
# 52dae0: 5407a8[i] += curve[i] * (1-w) (FMA)
|
||||||
|
# 52dbc0: memcpy 5407a8 → 540678
|
||||||
|
bands_output = curve * (1.0 - w)
|
||||||
|
|
||||||
|
return bands_output
|
||||||
|
|
||||||
|
|
||||||
|
def validate():
|
||||||
|
"""Validate against ptrace capture (chain_samples.pkl)."""
|
||||||
|
import pickle
|
||||||
|
path = '/tmp/opencode/winetrace_casc/chain_samples.pkl'
|
||||||
|
with open(path, 'rb') as f:
|
||||||
|
data = pickle.load(f)
|
||||||
|
|
||||||
|
s = data['samples']
|
||||||
|
cin = s[0]
|
||||||
|
cout = s[3]
|
||||||
|
|
||||||
|
trk = np.array(cin['trk'], dtype=np.float64)
|
||||||
|
b0_cout = np.array(cout['bands0'], dtype=np.float64)
|
||||||
|
|
||||||
|
# Fit w and n_iters
|
||||||
|
best_rms = 1e10
|
||||||
|
best_params = None
|
||||||
|
|
||||||
|
for n_iters in range(1, 11):
|
||||||
|
magnitudes = np.zeros(len(trk) // 2)
|
||||||
|
re = trk[0::2]; im = trk[1::2]
|
||||||
|
magnitudes = np.sqrt(re**2 + im**2)
|
||||||
|
|
||||||
|
curve = haar_smooth(magnitudes, n_iters)
|
||||||
|
|
||||||
|
sig = (curve > 0.5) & (b0_cout > 0.5)
|
||||||
|
if sig.sum() < 10:
|
||||||
|
continue
|
||||||
|
|
||||||
|
w_vals = 1.0 - b0_cout[sig] / curve[sig]
|
||||||
|
w = float(np.median(w_vals))
|
||||||
|
|
||||||
|
predicted = curve * (1.0 - w)
|
||||||
|
rms = float(np.sqrt(np.mean((predicted - b0_cout) ** 2)))
|
||||||
|
corr = float(np.corrcoef(curve[sig], b0_cout[sig])[0, 1])
|
||||||
|
|
||||||
|
if rms < best_rms:
|
||||||
|
best_rms = rms
|
||||||
|
best_params = (n_iters, w, corr)
|
||||||
|
|
||||||
|
print(f' iters={n_iters:2d}: w={w:.6f}, rms={rms:.4f}, corr={corr:.6f}')
|
||||||
|
|
||||||
|
n_iters, w, corr = best_params
|
||||||
|
print(f'\nBest: iters={n_iters}, w={w:.6f}, rms={best_rms:.4f}, corr={corr:.6f}')
|
||||||
|
return n_iters, w
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import sys
|
||||||
|
if '--validate' in sys.argv:
|
||||||
|
validate()
|
||||||
|
else:
|
||||||
|
print('Usage: detector_cascade.py --validate')
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
fit_vlaw_by_group.py — Fit VLAW parameters (α, β, c, Δ) per configuration group.
|
||||||
|
|
||||||
|
VLAW model (framed_model.cpp:205-208):
|
||||||
|
cs = α * log1p(lvl / β) + c + (delta ? Δ : 0)
|
||||||
|
applied_gain = 10^(-cs / 20) [gamma0=1 already absorbed into α,c,Δ]
|
||||||
|
|
||||||
|
Need to fit these for each (fc, q, sens) configuration group:
|
||||||
|
t1kq: fc=800..1200, q=1.0, sens=12 (input tone1kq)
|
||||||
|
t1k: fc=500..2000, q=1.0, sens=12 (input tone1k)
|
||||||
|
al: fc=1000, q=1.0, sens=3..24 (input lvl_tone_lvX)
|
||||||
|
res: fc=300..700, q=1.0, sens=12 (input resonant)
|
||||||
|
dual: fc=500, q=0.1..10.0, sens=12 (input dual)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
|
sys.path.insert(0, '/home/m/re-tools/scripts')
|
||||||
|
import corpus
|
||||||
|
|
||||||
|
corpus.RB = '/home/m/re-tools/dsp/build/render48k'
|
||||||
|
|
||||||
|
# Reference errors from baseline_bridge.json (target)
|
||||||
|
with open('scripts/baseline_bridge.json') as f:
|
||||||
|
REF_ERRORS = json.load(f)
|
||||||
|
|
||||||
|
def structural_cases():
|
||||||
|
out = []
|
||||||
|
for name, inp, args, ref, f in corpus.build_cases():
|
||||||
|
joined = [','.join(args)] if len(args) == 3 else args
|
||||||
|
out.append((name, inp, joined, ref, f))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def run_vlaw(inp, args, alpha, beta, c, delta):
|
||||||
|
"""Run render48k with VLAW parameters and return output path."""
|
||||||
|
out = f'/tmp/vlaw_fit_{alpha}_{beta}_{c}_{delta}_{os.path.basename(inp)}.wav'
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
'RT_VLAW': '1',
|
||||||
|
'RT_VLAW_ALPHA': str(alpha),
|
||||||
|
'RT_VLAW_BETA': str(beta),
|
||||||
|
'RT_VLAW_C': str(c),
|
||||||
|
'RT_VLAW_DELTA': str(delta),
|
||||||
|
'RT_SYN': '1',
|
||||||
|
'RT_NOWARP': '1',
|
||||||
|
'RT_NOIIR3': '1',
|
||||||
|
'RT_IIR12': '0',
|
||||||
|
}
|
||||||
|
subprocess.run(
|
||||||
|
[corpus.RB, inp, out] + args,
|
||||||
|
capture_output=True, text=True, env=env,
|
||||||
|
cwd='/home/m/re-tools'
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def eval_error(out, ref, f):
|
||||||
|
"""Evaluate error in dB between output and reference at frequency f."""
|
||||||
|
if not os.path.exists(out) or os.path.getsize(out) == 0:
|
||||||
|
return None
|
||||||
|
ref_sig = corpus.load_mono(ref)
|
||||||
|
out_sig = corpus.load_mono(out)
|
||||||
|
min_len = min(len(ref_sig), len(out_sig))
|
||||||
|
ref_sig = ref_sig[-min_len:]
|
||||||
|
out_sig = out_sig[-min_len:]
|
||||||
|
ref_ta = corpus.ta(ref_sig, f)
|
||||||
|
out_ta = corpus.ta(out_sig, f)
|
||||||
|
return corpus.db(out_ta / ref_ta)
|
||||||
|
|
||||||
|
def group_key(name):
|
||||||
|
return name.split('_')[0]
|
||||||
|
|
||||||
|
def evaluate_params(alpha, beta, c, delta, cases_subset=None):
|
||||||
|
"""Evaluate VLAW params on all cases, return per-group mean abs error."""
|
||||||
|
all_cases = structural_cases()
|
||||||
|
if cases_subset:
|
||||||
|
all_cases = [c for c in all_cases if group_key(c[0]) in cases_subset]
|
||||||
|
|
||||||
|
errs = {}
|
||||||
|
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
out = run_vlaw(inp, args, alpha, beta, c, delta)
|
||||||
|
err = eval_error(out, ref, f)
|
||||||
|
if err is not None:
|
||||||
|
errs[name] = err
|
||||||
|
|
||||||
|
# Group stats
|
||||||
|
groups = {}
|
||||||
|
for k, v in errs.items():
|
||||||
|
g = group_key(k)
|
||||||
|
groups.setdefault(g, []).append(v)
|
||||||
|
|
||||||
|
out_stats = {g: float(np.mean(np.abs(v))) for g, v in groups.items()}
|
||||||
|
out_stats['TOTAL'] = float(np.mean(np.abs(list(errs.values()))))
|
||||||
|
return out_stats, errs
|
||||||
|
|
||||||
|
def fit_single_case(name, inp, args, ref, f, init_params):
|
||||||
|
"""Grid search for best params on a single case."""
|
||||||
|
alpha0, beta0, c0, delta0 = init_params
|
||||||
|
best = None
|
||||||
|
best_err = float('inf')
|
||||||
|
|
||||||
|
# Search around initial params
|
||||||
|
alphas = np.linspace(max(0.5, alpha0-1), alpha0+1, 9)
|
||||||
|
betas = np.linspace(max(0.1, beta0-0.2), beta0+0.2, 9)
|
||||||
|
cs = np.linspace(max(0.0, c0-0.5), c0+0.5, 9)
|
||||||
|
deltas = np.linspace(max(0.0, delta0-2), delta0+2, 9)
|
||||||
|
|
||||||
|
for alpha in alphas:
|
||||||
|
for beta in betas:
|
||||||
|
for c in cs:
|
||||||
|
for delta in deltas:
|
||||||
|
out = run_vlaw(inp, args, alpha, beta, c, delta)
|
||||||
|
err = eval_error(out, ref, f)
|
||||||
|
if err is not None and abs(err) < best_err:
|
||||||
|
best_err = abs(err)
|
||||||
|
best = (alpha, beta, c, delta, err)
|
||||||
|
print(f' {name}: new best α={alpha:.3f}, β={beta:.3f}, c={c:.3f}, Δ={delta:.3f} => err={err:.3f} dB')
|
||||||
|
|
||||||
|
return best
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Build case map by group
|
||||||
|
all_cases = structural_cases()
|
||||||
|
groups = {}
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
g = group_key(name)
|
||||||
|
groups.setdefault(g, []).append((name, inp, args, ref, f))
|
||||||
|
|
||||||
|
print("Available groups:", list(groups.keys()))
|
||||||
|
for g, cases in groups.items():
|
||||||
|
print(f" {g}: {len(cases)} cases")
|
||||||
|
|
||||||
|
# Current calibrated params for dual(q=0.5)
|
||||||
|
dual_params = (3.2193, 0.4927, 0.5423, 6.9177)
|
||||||
|
|
||||||
|
# Test current params on all groups
|
||||||
|
print("\n=== Testing current dual params on all groups ===")
|
||||||
|
stats, _ = evaluate_params(*dual_params)
|
||||||
|
for g in ['t1kq', 't1k', 'al', 'res', 'dual', 'comb']:
|
||||||
|
if g in stats:
|
||||||
|
print(f' {g}: {stats[g]:.3f} dB')
|
||||||
|
|
||||||
|
# For each group, pick a representative case and fit
|
||||||
|
print("\n=== Fitting per group (representative case) ===")
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# For dual, use q=0.5 as reference (already calibrated)
|
||||||
|
if 'dual' in groups:
|
||||||
|
# Find q=0.5 case
|
||||||
|
for name, inp, args, ref, f in groups['dual']:
|
||||||
|
if '0.5' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['dual'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# For t1kq, use fc=1000
|
||||||
|
if 't1kq' in groups:
|
||||||
|
for name, inp, args, ref, f in groups['t1kq']:
|
||||||
|
if '1000' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['t1kq'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# For t1k, use fc=1000
|
||||||
|
if 't1k' in groups:
|
||||||
|
for name, inp, args, ref, f in groups['t1k']:
|
||||||
|
if '1000' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['t1k'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# For al, use sens=12
|
||||||
|
if 'al' in groups:
|
||||||
|
for name, inp, args, ref, f in groups['al']:
|
||||||
|
if '12' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['al'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# For res, use fc=500
|
||||||
|
if 'res' in groups:
|
||||||
|
for name, inp, args, ref, f in groups['res']:
|
||||||
|
if '500' in name:
|
||||||
|
best = fit_single_case(name, inp, args, ref, f, dual_params)
|
||||||
|
if best:
|
||||||
|
results['res'] = best[:4]
|
||||||
|
break
|
||||||
|
|
||||||
|
# Print results
|
||||||
|
print("\n=== FITTED VLAW PARAMETERS BY GROUP ===")
|
||||||
|
for g, (alpha, beta, c, delta) in results.items():
|
||||||
|
print(f'{g}: α={alpha:.4f}, β={beta:.4f}, c={c:.4f}, Δ={delta:.4f}')
|
||||||
|
|
||||||
|
# Save to JSON
|
||||||
|
with open('/tmp/opencode/vlaw_params.json', 'w') as f:
|
||||||
|
json.dump({g: {'alpha': a, 'beta': b, 'c': c, 'delta': d}
|
||||||
|
for g, (a, b, c, d) in results.items()}, f, indent=2)
|
||||||
|
print('\nSaved to /tmp/opencode/vlaw_params.json')
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
fit_vlaw_params.py — Fit VLAW parameters (α, β, c, Δ, γ₀) per configuration group.
|
||||||
|
|
||||||
|
VLAW model (framed_model.cpp:198-200):
|
||||||
|
cs = α * log1p(lvl / β) + c + (delta ? Δ : 0)
|
||||||
|
applied_gain = 10^(-γ₀ * cs / 20)
|
||||||
|
|
||||||
|
Currently hardcoded for dual(q=0.5): α=3.2193, β=0.4927, c=0.5423, Δ=7.46-0.5423, γ₀=1.79
|
||||||
|
|
||||||
|
Need to fit these for each (fc, q, sens) configuration group:
|
||||||
|
t1kq: fc=800..1200, q=1.0, sens=12
|
||||||
|
t1k: fc=500..2000, q=1.0, sens=12
|
||||||
|
al: fc=1000, q=1.0, sens=3..24
|
||||||
|
res: fc=300..700, q=1.0, sens=12
|
||||||
|
dual: fc=500, q=0.1..10.0, sens=12
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
sys.path.insert(0, '/home/m/re-tools/scripts')
|
||||||
|
import corpus
|
||||||
|
|
||||||
|
corpus.RB = '/home/m/re-tools/dsp/build/render48k'
|
||||||
|
|
||||||
|
def structural_cases():
|
||||||
|
out = []
|
||||||
|
for name, inp, args, ref, f in corpus.build_cases():
|
||||||
|
joined = [','.join(args)] if len(args) == 3 else args
|
||||||
|
out.append((name, inp, joined, ref, f))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def group_key(name):
|
||||||
|
return name.split('_')[0]
|
||||||
|
|
||||||
|
def load_ref_errors():
|
||||||
|
"""Load baseline_bridge.json for target errors."""
|
||||||
|
with open('scripts/baseline_bridge.json') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
def render_vlaw(inp, out, args, alpha, beta, c, delta, gamma0, extra_env=None):
|
||||||
|
"""Run render48k with VLAW parameters."""
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
'RT_VLAW': '1',
|
||||||
|
'RT_VLAW_ALPHA': str(alpha),
|
||||||
|
'RT_VLAW_BETA': str(beta),
|
||||||
|
'RT_VLAW_C': str(c),
|
||||||
|
'RT_VLAW_DELTA': str(delta),
|
||||||
|
'RT_VLAW_GAMMA0': str(gamma0),
|
||||||
|
'RT_SYN': '1',
|
||||||
|
'RT_NOWARP': '1',
|
||||||
|
'RT_NOIIR3': '1',
|
||||||
|
'RT_IIR12': '0',
|
||||||
|
}
|
||||||
|
if extra_env:
|
||||||
|
env.update(extra_env)
|
||||||
|
subprocess.run(
|
||||||
|
[corpus.RB, inp, out] + args,
|
||||||
|
capture_output=True, text=True, env=env,
|
||||||
|
cwd='/home/m/re-tools'
|
||||||
|
)
|
||||||
|
|
||||||
|
def eval_config(alpha, beta, c, delta, gamma0, cases_subset=None):
|
||||||
|
"""Evaluate VLAW params on cases, return per-group mean abs error."""
|
||||||
|
all_cases = structural_cases()
|
||||||
|
if cases_subset:
|
||||||
|
all_cases = [c for c in all_cases if group_key(c[0]) in cases_subset]
|
||||||
|
|
||||||
|
refs = load_ref_errors()
|
||||||
|
errs = {}
|
||||||
|
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
out = f'/tmp/vlaw_fit_{name}.wav'
|
||||||
|
render_vlaw(inp, out, args, alpha, beta, c, delta, gamma0)
|
||||||
|
|
||||||
|
if not os.path.exists(out) or os.path.getsize(out) == 0:
|
||||||
|
errs[name] = 999.0
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
ref_sig = corpus.load_mono(ref)
|
||||||
|
out_sig = corpus.load_mono(out)
|
||||||
|
min_len = min(len(ref_sig), len(out_sig))
|
||||||
|
ref_sig = ref_sig[-min_len:]
|
||||||
|
out_sig = out_sig[-min_len:]
|
||||||
|
|
||||||
|
ref_ta = corpus.ta(ref_sig, f)
|
||||||
|
out_ta = corpus.ta(out_sig, f)
|
||||||
|
err_db = corpus.db(out_ta / ref_ta)
|
||||||
|
errs[name] = err_db
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error on {name}: {e}")
|
||||||
|
errs[name] = 999.0
|
||||||
|
|
||||||
|
# Group stats
|
||||||
|
groups = {}
|
||||||
|
for k, v in errs.items():
|
||||||
|
g = group_key(k)
|
||||||
|
groups.setdefault(g, []).append(v)
|
||||||
|
|
||||||
|
out = {g: float(np.mean(np.abs(v))) for g, v in groups.items()}
|
||||||
|
out['TOTAL'] = float(np.mean(np.abs(list(errs.values()))))
|
||||||
|
return out, errs
|
||||||
|
|
||||||
|
def fit_alpha_beta_c(cases_to_fit):
|
||||||
|
"""Coordinate descent on (α, β, c) for a specific case group."""
|
||||||
|
# For now, grid search
|
||||||
|
best = None
|
||||||
|
best_err = float('inf')
|
||||||
|
|
||||||
|
# Search ranges around current dual(q=0.5) values
|
||||||
|
for alpha in np.linspace(2.5, 4.0, 8):
|
||||||
|
for beta in np.linspace(0.3, 0.7, 8):
|
||||||
|
for c in np.linspace(0.2, 1.0, 8):
|
||||||
|
stats, _ = eval_config(alpha, beta, c, 6.9, 1.79, cases_to_fit)
|
||||||
|
total = stats['TOTAL']
|
||||||
|
if total < best_err:
|
||||||
|
best_err = total
|
||||||
|
best = (alpha, beta, c, stats)
|
||||||
|
print(f" New best: α={alpha:.4f}, β={beta:.4f}, c={c:.4f}, TOTAL={total:.4f}")
|
||||||
|
|
||||||
|
return best
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Build case map by group
|
||||||
|
all_cases = structural_cases()
|
||||||
|
groups = {}
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
g = group_key(name)
|
||||||
|
groups.setdefault(g, []).append(name)
|
||||||
|
|
||||||
|
print("Available groups:", list(groups.keys()))
|
||||||
|
for g, names in groups.items():
|
||||||
|
print(f" {g}: {len(names)} cases")
|
||||||
|
|
||||||
|
# Start with dual group (already calibrated)
|
||||||
|
print("\n=== Testing dual(q=0.5) baseline ===")
|
||||||
|
stats, errs = eval_config(3.2193, 0.4927, 0.5423, 6.9177, 1.79, ['dual'])
|
||||||
|
print(f"Dual stats: {stats}")
|
||||||
|
|
||||||
|
# Now fit for each group
|
||||||
|
for g in ['t1kq', 't1k', 'al', 'res', 'dual']:
|
||||||
|
if g not in groups:
|
||||||
|
continue
|
||||||
|
print(f"\n=== Fitting {g} ===")
|
||||||
|
best = fit_alpha_beta_c([g])
|
||||||
|
if best:
|
||||||
|
alpha, beta, c, stats = best
|
||||||
|
print(f" {g} best: α={alpha:.4f}, β={beta:.4f}, c={c:.4f}")
|
||||||
|
print(f" Stats: {stats}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Quick VLAW parameter grid search - test fewer combos per case.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
|
sys.path.insert(0, '/home/m/re-tools/scripts')
|
||||||
|
import corpus
|
||||||
|
|
||||||
|
corpus.RB = '/home/m/re-tools/dsp/build/render48k'
|
||||||
|
|
||||||
|
with open('scripts/baseline_bridge.json') as f:
|
||||||
|
REF_ERRORS = json.load(f)
|
||||||
|
|
||||||
|
def structural_cases():
|
||||||
|
out = []
|
||||||
|
for name, inp, args, ref, f in corpus.build_cases():
|
||||||
|
joined = [','.join(args)] if len(args) == 3 else args
|
||||||
|
out.append((name, inp, joined, ref, f))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def run_vlaw(inp, args, alpha, beta, c, delta):
|
||||||
|
out = f'/tmp/vlaw_{alpha}_{beta}_{c}_{delta}_{os.path.basename(inp)}.wav'
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
'RT_VLAW': '1',
|
||||||
|
'RT_VLAW_ALPHA': str(alpha),
|
||||||
|
'RT_VLAW_BETA': str(beta),
|
||||||
|
'RT_VLAW_C': str(c),
|
||||||
|
'RT_VLAW_DELTA': str(delta),
|
||||||
|
'RT_SYN': '1', 'RT_NOWARP': '1', 'RT_NOIIR3': '1', 'RT_IIR12': '0',
|
||||||
|
}
|
||||||
|
subprocess.run([corpus.RB, inp, out] + args, capture_output=True, env=env, cwd='/home/m/re-tools')
|
||||||
|
return out
|
||||||
|
|
||||||
|
def eval_error(out, ref, f):
|
||||||
|
if not os.path.exists(out) or os.path.getsize(out) == 0:
|
||||||
|
return None
|
||||||
|
ref_sig = corpus.load_mono(ref)
|
||||||
|
out_sig = corpus.load_mono(out)
|
||||||
|
min_len = min(len(ref_sig), len(out_sig))
|
||||||
|
ref_sig = ref_sig[-min_len:]
|
||||||
|
out_sig = out_sig[-min_len:]
|
||||||
|
ref_ta = corpus.ta(ref_sig, f)
|
||||||
|
out_ta = corpus.ta(out_sig, f)
|
||||||
|
return corpus.db(out_ta / ref_ta)
|
||||||
|
|
||||||
|
def group_key(name):
|
||||||
|
return name.split('_')[0]
|
||||||
|
|
||||||
|
all_cases = structural_cases()
|
||||||
|
groups = {}
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
g = group_key(name)
|
||||||
|
groups.setdefault(g, []).append((name, inp, args, ref, f))
|
||||||
|
|
||||||
|
# Pick one case per group
|
||||||
|
rep_cases = {}
|
||||||
|
for g in ['t1kq', 't1k', 'al', 'res', 'dual']:
|
||||||
|
if g in groups:
|
||||||
|
# Pick middle-ish case
|
||||||
|
cases = groups[g]
|
||||||
|
rep_cases[g] = cases[len(cases)//2]
|
||||||
|
|
||||||
|
print("Representative cases:")
|
||||||
|
for g, (name, inp, args, ref, f) in rep_cases.items():
|
||||||
|
print(f" {g}: {name}")
|
||||||
|
|
||||||
|
# Test a small grid around dual params
|
||||||
|
dual_params = (3.2193, 0.4927, 0.5423, 6.9177)
|
||||||
|
|
||||||
|
print("\n=== Grid search per group ===")
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for g, (name, inp, args, ref, f) in rep_cases.items():
|
||||||
|
print(f"\n--- {g} ({name}) ---")
|
||||||
|
best = None
|
||||||
|
best_err = float('inf')
|
||||||
|
|
||||||
|
# Coarse grid
|
||||||
|
alphas = np.linspace(1.0, 5.0, 5)
|
||||||
|
betas = np.linspace(0.2, 0.8, 5)
|
||||||
|
cs = np.linspace(-0.5, 2.0, 5)
|
||||||
|
deltas = np.linspace(0.0, 12.0, 5)
|
||||||
|
|
||||||
|
for alpha in alphas:
|
||||||
|
for beta in betas:
|
||||||
|
for c in cs:
|
||||||
|
for delta in deltas:
|
||||||
|
out = run_vlaw(inp, args, alpha, beta, c, delta)
|
||||||
|
err = eval_error(out, ref, f)
|
||||||
|
if err is not None and abs(err) < best_err:
|
||||||
|
best_err = abs(err)
|
||||||
|
best = (alpha, beta, c, delta, err)
|
||||||
|
print(f' {name}: α={alpha:.3f}, β={beta:.3f}, c={c:.3f}, Δ={delta:.3f} => {err:.3f} dB')
|
||||||
|
|
||||||
|
if best:
|
||||||
|
results[g] = best[:4]
|
||||||
|
print(f' BEST {g}: α={best[0]:.4f}, β={best[1]:.4f}, c={best[2]:.4f}, Δ={best[3]:.4f} => {best[4]:.3f} dB')
|
||||||
|
|
||||||
|
print("\n=== SUMMARY ===")
|
||||||
|
for g, (a, b, c, d) in results.items():
|
||||||
|
print(f'{g}: α={a:.4f}, β={b:.4f}, c={c:.4f}, Δ={d:.4f}')
|
||||||
|
|
||||||
|
with open('/tmp/opencode/vlaw_params.json', 'w') as f:
|
||||||
|
json.dump({g: {'alpha': a, 'beta': b, 'c': c, 'delta': d}
|
||||||
|
for g, (a, b, c, d) in results.items()}, f, indent=2)
|
||||||
|
print('\nSaved to /tmp/opencode/vlaw_params.json')
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Quick VLAW parameter test - just evaluate a few configs per group.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
sys.path.insert(0, '/home/m/re-tools/scripts')
|
||||||
|
import corpus
|
||||||
|
|
||||||
|
corpus.RB = '/home/m/re-tools/dsp/build/render48k'
|
||||||
|
|
||||||
|
def run_one(inp, args, alpha, beta, c, delta, gamma0=1.79):
|
||||||
|
out = f'/tmp/vlaw_test_{alpha}_{beta}_{c}.wav'
|
||||||
|
env = {
|
||||||
|
**os.environ,
|
||||||
|
'RT_VLAW': '1',
|
||||||
|
'RT_VLAW_ALPHA': str(alpha),
|
||||||
|
'RT_VLAW_BETA': str(beta),
|
||||||
|
'RT_VLAW_C': str(c),
|
||||||
|
'RT_VLAW_DELTA': str(delta),
|
||||||
|
'RT_SYN': '1',
|
||||||
|
'RT_NOWARP': '1',
|
||||||
|
'RT_NOIIR3': '1',
|
||||||
|
'RT_IIR12': '0',
|
||||||
|
}
|
||||||
|
subprocess.run(
|
||||||
|
[corpus.RB, inp, out] + args,
|
||||||
|
capture_output=True, text=True, env=env,
|
||||||
|
cwd='/home/m/re-tools'
|
||||||
|
)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def eval_one(name, inp, args, ref, f, alpha, beta, c, delta):
|
||||||
|
out = run_one(inp, args, alpha, beta, c, delta)
|
||||||
|
if not os.path.exists(out) or os.path.getsize(out) == 0:
|
||||||
|
return None
|
||||||
|
ref_sig = corpus.load_mono(ref)
|
||||||
|
out_sig = corpus.load_mono(out)
|
||||||
|
min_len = min(len(ref_sig), len(out_sig))
|
||||||
|
ref_sig = ref_sig[-min_len:]
|
||||||
|
out_sig = out_sig[-min_len:]
|
||||||
|
ref_ta = corpus.ta(ref_sig, f)
|
||||||
|
out_ta = corpus.ta(out_sig, f)
|
||||||
|
return corpus.db(out_ta / ref_ta)
|
||||||
|
|
||||||
|
# Test current VLAW params on different groups
|
||||||
|
test_params = (3.2193, 0.4927, 0.5423, 6.9177)
|
||||||
|
|
||||||
|
all_cases = []
|
||||||
|
for name, inp, args, ref, f in corpus.build_cases():
|
||||||
|
joined = [','.join(args)] if len(args) == 3 else args
|
||||||
|
all_cases.append((name, inp, joined, ref, f))
|
||||||
|
|
||||||
|
# Pick one representative case per group
|
||||||
|
groups = {}
|
||||||
|
for name, inp, args, ref, f in all_cases:
|
||||||
|
g = name.split('_')[0]
|
||||||
|
if g not in groups:
|
||||||
|
groups[g] = (name, inp, args, ref, f)
|
||||||
|
|
||||||
|
print("Testing VLAW params (3.2193, 0.4927, 0.5423, 6.9177) on each group:")
|
||||||
|
for g, (name, inp, args, ref, f) in groups.items():
|
||||||
|
err = eval_one(name, inp, args, ref, f, *test_params)
|
||||||
|
if err is not None:
|
||||||
|
print(f" {name} ({g}): {err:.3f} dB")
|
||||||
|
else:
|
||||||
|
print(f" {name} ({g}): FAILED")
|
||||||
Reference in New Issue
Block a user