Compare commits
22
Commits
d97dcffaa7
...
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d75f4d22 | ||
|
|
588d2dcc36 | ||
|
|
4ed3481166 | ||
|
|
8805a8f183 | ||
|
|
d7cbab3e4c | ||
|
|
1ea4bf6480 | ||
|
|
f689023089 | ||
|
|
575d26a771 | ||
|
|
4d78785f0b | ||
|
|
b6e7fdc289 | ||
|
|
f68081f694 | ||
|
|
c18f4b3ef4 | ||
|
|
7202b8d6a4 | ||
|
|
943e781720 | ||
|
|
60421c32c9 | ||
|
|
6bc0120286 | ||
|
|
c69257a551 | ||
|
|
50d7ab0d05 | ||
|
|
3c5e276fc5 | ||
|
|
f0cfec8af7 | ||
|
|
c2da7495c0 | ||
|
|
a281f6a721 |
@@ -29,6 +29,7 @@ add_library(soothe2_dsp SHARED
|
||||
fn529fe0.cpp
|
||||
rt_weights.cpp
|
||||
rt_mask_tables.cpp
|
||||
log2_ln.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);
|
||||
}
|
||||
|
||||
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_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
|
||||
// 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).
|
||||
//
|
||||
// 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 {
|
||||
|
||||
// ---- 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) {
|
||||
// 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).
|
||||
|
||||
@@ -18,6 +18,57 @@
|
||||
// (level = am/res) but fed through the structural chain instead of the LUT bridge.
|
||||
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).
|
||||
// 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);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// - 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
|
||||
// - cascade : Haar, magnitudes, blend (529c60 decode)
|
||||
int main() {
|
||||
const size_t nbin = 2049; // internal N/2+1 grid used by the chain
|
||||
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",
|
||||
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");
|
||||
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;
|
||||
}
|
||||
|
||||
// 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)
|
||||
std::vector<float> raw_level(nbin);
|
||||
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.
|
||||
// cutS(b) = 1.729*ln(1 + lvl_raw/0.3824) + Delta(b) [stage-S]
|
||||
// applied gain = 10^(-gamma0*cutS/20), gamma0 = 1.79
|
||||
// cutS(b) = alpha * ln(1 + lvl_raw / beta) + c + Delta(b) [stage-S]
|
||||
// applied gain = 10^(-gamma0 * cutS / 20)
|
||||
// Delta-branch: neighbourhoods of off-center content peaks get +4.18 dB.
|
||||
// Bypasses LUT/exp2/blend/warp/IIR3 entirely.
|
||||
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;
|
||||
}
|
||||
}
|
||||
// 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++) {
|
||||
// Applied-stage law (NOTES 24s): direct fit of deep-scratch vs lvl.
|
||||
double cs = 3.2193 * std::log1p(raw_level[k2] / 0.4927)
|
||||
+ 0.5423
|
||||
+ (delta_mark[k2] ? (7.46 - 0.5423) : 0.0);
|
||||
// Applied-stage law: direct fit of deep-scratch vs lvl
|
||||
double cs = vlaw_alpha * std::log1p(raw_level[k2] / vlaw_beta)
|
||||
+ vlaw_c
|
||||
+ (delta_mark[k2] ? vlaw_delta : 0.0);
|
||||
band_level[k2] = static_cast<float>(std::pow(10.0, -cs / 20.0));
|
||||
}
|
||||
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
|
||||
|
||||
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;
|
||||
res_.clear();
|
||||
track_.clear();
|
||||
twin_resp_complex_.clear();
|
||||
cascade_states_.clear();
|
||||
|
||||
// RT_DUMPRESPATH=<file> (NOTES 22t): static twin-response spectra per band,
|
||||
// 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::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) {
|
||||
int32_t bi = static_cast<int32_t>(res_.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);
|
||||
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) {
|
||||
@@ -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;
|
||||
|
||||
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,
|
||||
sample_rate_, sf, fparams,
|
||||
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 {
|
||||
process_band_structural(am_.data(), res_[b].data(), bands_[b],
|
||||
band_mask.data(), nfft_, sample_rate_);
|
||||
}
|
||||
}
|
||||
for (size_t k = 0; k <= half; k++) {
|
||||
mask[k] = std::min(band_mask[k], mask[k]);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <cstddef>
|
||||
#include <complex>
|
||||
#include <vector>
|
||||
#include "fn529fe0.hpp"
|
||||
|
||||
struct DetectorBand {
|
||||
float fc; // band center freq (Hz)
|
||||
@@ -71,6 +72,11 @@ public:
|
||||
|
||||
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:
|
||||
size_t nfft_;
|
||||
float sample_rate_;
|
||||
@@ -82,4 +88,10 @@ private:
|
||||
std::vector<float> am_; // smoothed per-bin amplitude
|
||||
std::vector<float> f6f8_; // shared 0x5406f8 blend buffer (IIR1 out)
|
||||
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 "fftconv.hpp"
|
||||
#include "log2_ln.hpp"
|
||||
#include "exp2_tables.hpp"
|
||||
#include "exp2.hpp"
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
|
||||
SpectralProcessor::SpectralProcessor(size_t nfft, size_t hop, float sample_rate)
|
||||
: nfft_(nfft), hop_(hop), frame_count_(0), output_pos_(0),
|
||||
@@ -36,6 +40,7 @@ SpectralProcessor::~SpectralProcessor() {
|
||||
|
||||
void SpectralProcessor::setDetectorParams(const std::vector<DetectorBand>& bands) {
|
||||
detector_.setParams(bands);
|
||||
loadWinFreq();
|
||||
}
|
||||
|
||||
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) {
|
||||
memset(out, 0, num_samples * sizeof(float));
|
||||
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;
|
||||
}
|
||||
} else if (firconv) {
|
||||
// RT_FIRCONV=1: Build FIR from mask and apply via complex multiply.
|
||||
// The mask is real-valued (per-bin gain). We apply it directly
|
||||
// to the audio spectrum via complex multiply (th_b3c0 equivalent).
|
||||
// No upper-half zeroing — preserve Hermitian symmetry.
|
||||
// RT_FIRCONV=2: Full FIR construction pipeline (52b550-52b8bb).
|
||||
// mask → reciprocal (1/mask) → window → normalize → complex multiply.
|
||||
// This replicates the plugin's FFT-conv FIR design path.
|
||||
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++) {
|
||||
fir_freq_[i] = std::complex<double>(
|
||||
static_cast<double>(mask_[i % (nfft_/2+1)]), 0.0);
|
||||
}
|
||||
|
||||
// Complex multiply FIR × audio spectrum.
|
||||
for (size_t i = 0; i < nfft_; i++) {
|
||||
buf_[i] *= fir_freq_[i];
|
||||
}
|
||||
|
||||
@@ -37,4 +37,14 @@ private:
|
||||
void computeWindow();
|
||||
void stftFrame(const float* in, std::complex<double>* out);
|
||||
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();
|
||||
};
|
||||
|
||||
@@ -698,3 +698,95 @@ mask = exp(2 · scratch) ⇒ γ = 2·k, где k — масштаб выхода
|
||||
3. Масштаб выхода design: декод хвоста 1802a24c0 (файл уже есть:
|
||||
nls_dasm/conv_float_a24c0.dis, 184K).
|
||||
4. Согласование с identity-фазой захватов (гонка финального combine).
|
||||
|
||||
## ДОПОЛНЕНИЕ 24mm7: design выход = точный ln(bands_final); γ создаётся после design
|
||||
|
||||
### Численный тест (multi6/ph034, identity-фаза)
|
||||
```
|
||||
scr@628 − ln(cur@678): max|r| = 9.0e-08 (float32 eps) на 1013 бинах
|
||||
⇒ k_design = 1 (в момент захвата)
|
||||
```
|
||||
Оговорка: станционарность делает «свежий» и «сталый» scratch
|
||||
неразличимы; но факт (scr, cur)=(лог, значение) одной пары твёрд.
|
||||
|
||||
### Следствие для γ
|
||||
γ=1.760561 ≠ 2 ⇒ множитель НЕ только «×2 перед EXP». Источники:
|
||||
(a) opB/opC не взаимно сокращаются (не чистая упаковка real-FFT);
|
||||
(b) пост-exp шаги: окно 52d990 (варьируется по позиции — нарушил бы
|
||||
степенной закон, значит действует на верхнюю половину/после),
|
||||
pair-scalar th1880/th1ca0, финальный combine df0(FIR, track_i),
|
||||
где track=exp(scr)=bands_final.
|
||||
Комбинации дающие γ из {1,2}: 1+2x=1.760561 ⇒ x=0.3802805;
|
||||
либо лог-доменное смешение track^a·FIR^b c a+2b=1.760561.
|
||||
|
||||
### Статус декода design 1802a24c0
|
||||
AVX-512 (zmm, masked {k3}/{k4}), 3822 строки objdump — трансформ-класс.
|
||||
Для замыкания γ его полный декод МОЖНО НЕ НУЖЕН: достаточно семантики
|
||||
opB/opC + df0 (десятки инструкций в fn529fe0.dis).
|
||||
|
||||
## ДОПОЛНЕНИЕ 24mm8 (финал захода): opB/opC/df0 резолвлены
|
||||
```
|
||||
opB: 180002180→180004ca80(f)/18001d160(d) ; дескриптор-оп (тег [obj]==6)
|
||||
opC: 180001a90→18001a0c0(f)/180018400(d)
|
||||
df0: 18000df0→18000b3c0 ; f70→18000e360 ; финальный combine
|
||||
```
|
||||
Все четыре микровопроса 24mm6 закрыты или локализованы до тел-обёрток.
|
||||
Следующий раунд: семантика 4ca80/1a0c0 (кандидаты источника γ=2k−масштаба),
|
||||
затем полный numpy-конвейер.
|
||||
|
||||
## ДОПОЛНЕНИЕ 24mm9: opB/opC = RFFT-близнецы; df0 = complex-mul; цепь валидирована 0.0065 дБ
|
||||
|
||||
### Слой вызовов FIR-секции (уточнение поверх 24l/24mm6)
|
||||
```
|
||||
обёртки: th2180 impl=125e0, th1a90 impl=5560 — только перестановка аргументов:
|
||||
воркер получает (rcx=data, rdx=data, r8=ПЛАН, r9=WORK), ин-плейс.
|
||||
ПЛАН = [ctx+540548] (buf548!): tag=6 [+0], log2n=12 [+4], flag [+8]=0,
|
||||
scale_flag=1 [+0xc], scale=2^-12 [+0x10], workbytes=16384 [+0x18].
|
||||
WORK = [ctx+540598] — рабочая область FFT (заметение «lane-mask» из 23b).
|
||||
th2180 → воркер 4ca80(f)/1d160(d): INVERSE real-RFFT (голова: X[0]±X[Nyq]).
|
||||
th1a90 → воркер 1a0c0(f)/18400(d): FORWARD real-RFFT (хвост: пакинг Nyq).
|
||||
тела: импортные близнецы 181b853e0(inv)/181b81b80(fwd); константы только
|
||||
±0.707107; масштабов нет. ffe0 = ×scale pass (skip при scale∈{0,1}).
|
||||
copy th2210 → 136e0 → 4d900(src,dst,n): pack re=v, im=0 (vunpcklps+zero).
|
||||
df0 18000b3c0: ПОЭЛЕМЕНТНОЕ КОМПЛЕКСНОЕ УМНОЖЕНИЕ dst=[rdx]=arg2:
|
||||
track_i := track_i ⊗ FIR (vfmaddsub213ps; f70/b560 — double версия).
|
||||
EXP 140b30 → 1803831c0: полиномиальная комплексная exp (без таблиц значений):
|
||||
magic 12582912 (=2^23·1.5), guard 87.33654, редукция 184.665≈128/ln2,
|
||||
коэф. {−0.01604,−1.541667(=−37/24), 3.166e-05, 1.008329, −1.65777e-06,
|
||||
−0.01932, 0.00134, 0.00541687, 10000, 4.19179}; AVX-512+FMA.
|
||||
Численно = поточечный комплексный exp (flat-exp проигрывает 8 дБ).
|
||||
```
|
||||
|
||||
### Полная последовательность (52b60c–52b893, все шаги, без пропусков)
|
||||
```
|
||||
design 535a70(scratch@628 ← ln(bands_i)) ; 52b62f, своп аргументов
|
||||
copy 2210(scratch → FIR, 2049 пар (re,im=0)) ; 52b644
|
||||
FIR[4096]=0 ; 52b685 Найквост ДО фолда
|
||||
inv-RFFT opA ; 52b672 th2180
|
||||
fold: float[1..2047]*=2.0 (xmm13@1824c41e0) ; 52d920
|
||||
float[2049..4095]=0 ; 52db50
|
||||
fwd-RFFT opB ; 52b6e1 th1a90
|
||||
EXP in-place, аргумент×q (q≈0.80, источник ОТКРЫТ); 52b716
|
||||
inv-RFFT opC ; 52b74b th2180
|
||||
FIR[4096]=0 ; 52b76d
|
||||
float[0..2047]*=WINfreq[2048..4095] ; 52d990 (падающий Hann)
|
||||
float[2048..4095]=0 ; 52db50
|
||||
fwd-RFFT opD ; 52b7ba th1a90
|
||||
FIR[0]=1.0f; FIR[1]=0 ; 52b7cd
|
||||
(flag f890≠0: pair-scalars 1880/ca0 — live мертво)
|
||||
th2030(FIR, wet=s888, 2n float) ; 52b857, s888=1 no-op
|
||||
df0(FIR, track_i, n): track_i := track_i ⊗ FIR ; 52b893
|
||||
```
|
||||
Смысл: классическое минимально-фазовое ядро через кепстр
|
||||
(IDFT лога → фолдинг ×2 причинной части + усечение → exp → обратный ход).
|
||||
|
||||
### Валидация и γ
|
||||
mask_sim = trk·|F(q)|: 60 ультрачистых кадров, ВСЕ 2049 бина:
|
||||
rms мед 0.0065 дБ / p90 0.0075 / max 0.035 при q=0.80 (порог 0.05 ✓).
|
||||
γ = 1 + s_F(q), s_F = наклон log|F| по log trk в нотче: q=0.8 ⇒ γ_pred=1.7516
|
||||
(точный 1.760561). Открыто: место q в асме (внутренность 1803831c0);
|
||||
unicorn не эмулирует FMA ⇒ нужен статдекод ядра или live-захват входа EXP.
|
||||
Дизасмы: /tmp/opencode/cascade/{wrapA_125e0,wrapB_5560,opB_4ca80,opC_1a0c0,
|
||||
df0_b3c0,h_ffe0,h_136e0*,imp_b8*3e0_full,bk_1803831c0}.dis
|
||||
(*copy: python3 scripts/disasm_func.py 1800136e0 — ВАЖНО: полный VA,
|
||||
короткая форма «125e0» даёт пустой файл!).
|
||||
|
||||
@@ -4021,3 +4021,393 @@ X ∈ {lvl, am·S/res, am·S, S/res, am·res, S·lvl, am·S/√res}; лучши
|
||||
3. Симулятор: scripts/cascade_sim.py (каркас + валидатор + отбор кадров).
|
||||
Следующий раунд: оп-за-оп декод тела каскада между lvl_raw и scr
|
||||
(частотные IIR с коэф. 24cc → vec6f8/bands_curve), затем C++ порт.
|
||||
|
||||
## 24mm5–24mm7 (продолжение захода)
|
||||
1. Полная буферная карта полосы-цикла: ДВА круга log→exp, между вторыми —
|
||||
бидир-IIR ×2 В ЛОГ-ДОМЕНЕ (= спектральное смешение детектора).
|
||||
2. Design 535a70 (своп аргументов) → 1802a24c0 = тело conv из вопроса 22z.
|
||||
Вход [r15]=bands_final@678+i, выход scratch@628.
|
||||
3. Тест: scr == ln(bands_final) с eps float32 (identity-фаза) ⇒ k=1.
|
||||
4. Перед EXP: FIR[1..n/2] *= 2.0 (@1824c41e0; исправление «÷−1» из 24l).
|
||||
5. γ=1.760561 возникает после design: кандидаты opB/opC и финальный
|
||||
combine df0(FIR, track_i); формулы-кандидаты в BLOCKMAP 24mm7.
|
||||
6. Все правки dataflow 24hh/24ii/24l перенесены в BLOCKMAP (14 порядок,
|
||||
ACC@7c8, ×2 вместо ÷−1, log#1/log#2 позиции).
|
||||
|
||||
## 24mm8-бис: точка входа следующего раунда
|
||||
opB-воркер 18004ca80: пролог + тег [obj]==6 проверка (0xfffffff3 при сбое),
|
||||
дальше — дескрипторная операция над векторами (BLOCKMAP 24l «ops A–D»).
|
||||
Полный дизасм не сохранён (скрипт-глюк с редиректом), перегенерировать:
|
||||
`python3 scripts/disasm_func.py 18004ca80 0x300`.
|
||||
Цель: найти множитель γ=1.760561 внутри opB/opC/df0 → замкнуть конвейер.
|
||||
|
||||
## ============ 24mm9: FIR-ЦЕПЬ ДЕКОДИРОВАНА = MIN-PHASE КЕПСТРАЛЬНЫЙ СЭНДВИЧ; ВАЛИДАЦИЯ 0.0065 дБ ============
|
||||
|
||||
### Структура опов (полные дизасмы в /tmp/opencode/cascade/*.dis)
|
||||
```
|
||||
обёртки 125e0/5560: перестановка аргументов → воркер(rcx=data, rdx=data,
|
||||
r8=ПЛАН, r9=WORK); всё ин-плейс над FIR
|
||||
ПЛАН = buf548@540548 (!НЕ маски/твидлы): {tag=6, log2n=12 [+4],
|
||||
[+8]=0, scale_flag=1 [+0xc], scale=2^-12 [+0x10], work=16384Б [+0x18]}
|
||||
WORK = buf598@540598 (рабочая область FFT, ~1.0 мусор; «lane-mask» из
|
||||
23b — УСТАРЕЛО)
|
||||
th2180(воркер 4ca80) = INVERSE real-FFT (голова: сумма/разность X[0]/X[Nyq]);
|
||||
th1a90(воркер 1a0c0) = FORWARD real-FFT (хвост: пакинг Найквиста в слот n);
|
||||
тела — импортные близнецы 181b853e0/181b81b80 (AVX, только ±1/√2 твидлы,
|
||||
без внутренних масштабов); ffe0 = in-place ×scale (skip при 1/0);
|
||||
copy th2210→136e0→4d900 = пак real→interleaved complex (re=v, im=0).
|
||||
df0 18000b3c0 = ПОЭЛЕМЕНТНОЕ КОМПЛЕКСНОЕ УМНОЖЕНИЕ, dst=arg2:
|
||||
track_i := track_i ⊗ FIR (vfmaddsub213ps; двойная версия b560+)
|
||||
EXP-ядро 140b30→1803831c0: таблично-полиномиальная комплексная экспонента
|
||||
(магия 12582912 expf-класса, guard 87.33654=maxarg−2ln2,
|
||||
редукция 184.665≈128/ln2, диадич. {−37/24,15/8,31/24,11/1024});
|
||||
численно ведёт себя как КОМПЛЕКСНЫЙ exp (вариант flat проиграл 8 дБ).
|
||||
Unicorn-эмуляция невозможна (нет FMA в TCG) — статический декод открыт.
|
||||
```
|
||||
|
||||
### Цепь (все константы из асмa; fn529fe0.dis 52b60c–52b893)
|
||||
```
|
||||
scr@628 → pack(re=scr,im=0) 2049 пар
|
||||
→ FIR[n]=FIR[4096]=0 ; 52b685 (Найквост re:=0, ДО фолда!)
|
||||
→ inv-RFFT ; opA th2180
|
||||
→ fold: y[1..2047]*=2.0 ; xmm13 @1824c41e0, 52d920
|
||||
y[2049..4095]=0 ; 52db50 (y[2048] НЕ трогается)
|
||||
→ fwd-RFFT ; opB th1a90
|
||||
→ комплексная EXP аргумент ×q ; 52b716, q≈0.80 (см. ОТКРЫТО)
|
||||
→ inv-RFFT ; opC th2180
|
||||
→ float[0..2047]*=WINfreq[2048..4095] (падающий Hann); хвост=0 ; 52d990/db50
|
||||
→ fwd-RFFT ; opD th1a90
|
||||
→ FIR[0]=1.0, FIR[1]=0 ; 52b7cd
|
||||
→ df0: mask_i := track_i ⊗ FIR ; финальный combine
|
||||
```
|
||||
Это классическое построение минимально-фазового ядра через кепстр
|
||||
(IDFT лога → удвоение причинной части → exp → обратно).
|
||||
|
||||
### Валидация (структурная фаза, критерий <0.05 дБ — ВЫПОЛНЕН)
|
||||
60 ультрачистых кадров (|γ_fit−1.760561|<5e-4, fit-rms<1e-5, все sc_*):
|
||||
по ВСЕМ 2049 бинам rms медиана **0.0065 дБ**, p90 0.0075, max 0.035 при q=0.80.
|
||||
Инструмент: cascade_sim.py --mask <ds> / fir_probe.py.
|
||||
|
||||
### γ выводится из цепи: γ = 1 + s_F(q), s_F = ∂log|F|/∂log trk в нотче
|
||||
При q=0.8: s_F=0.7516 ⇒ γ_pred=1.7516 против точного 1.760561 (Δ 0.5%).
|
||||
Остаточная структура та же, что даёт пер-бин модуляцию rms 0.0065 дБ.
|
||||
|
||||
### ОТКРЫТО (следующий раунд)
|
||||
1. **Источник q** на аргументе EXP: эмпирика 0.785–0.809 по подвыборкам;
|
||||
главный подозреваемый — внутренность 1803831c0 (или xmm10=0.8
|
||||
@1824c3e28-класс константа вне прослеженного пути). Нужен статический
|
||||
декод ядра (~5700 строк AVX-512+FMA) либо live-захват входа/выхода EXP.
|
||||
2. Асинхронность снапшотов (cur может отставать от scr/trk) ограничивает
|
||||
точность пер-кадрового фита — уйдёт с симуляцией детектора (Этап B).
|
||||
3. Детекторный каскад lvl_raw→scr (шаги 9–17) не тронут; нужны ACC@7c8
|
||||
(+ WINfreq@658 для контроля окна) — обновить rendersnap2 SLOTS.
|
||||
|
||||
## ============ 24mm10: ЯДРО EXP ДЕКОДИРОВАНО ПОЛНОСТЬЮ; МАСШТАБЫ ОПОВ ЗАФИКСИРОВАНЫ; ПАРАДОКС q ============
|
||||
|
||||
### EXP 1803831c0 — полная формула (скалярный путь = векторная математика)
|
||||
Вход: пары (re,im) плоско; выход in-place. Это ТОЧНАЯ комплексная экспонента:
|
||||
```
|
||||
e^re: t = fma(re, C1, MAGIC), C1=184.665 (=128·log2e!), MAGIC=12582912
|
||||
k = t−MAGIC (округление до целого); таблица T @0x1820fcd80,
|
||||
запись 8 байт (hi/lo extended double), индекс (t&MASK)<<3
|
||||
r_hi = re − k·h, h=0.00541687 (=ln2/128, f32)
|
||||
r = r_hi + k·1.65777e-06 ; Коди–Уэйт вторая компонента
|
||||
p = r + 0.5·r²
|
||||
e^re = T[k].lo + T[k].hi·p (+знаковые фиксы, guard 87.33654=maxarg−2ln2)
|
||||
e^(i·im): |im|→q=round(|im|·(1/π)) через тот же MAGIC; приведение в DOUBLE:
|
||||
rπ = |im| − q·π_hi − q·π_lo (двухкомпонентный π); фолд π/2;
|
||||
минимакс sin/cos в double {2.60578e-06, −1.98096e-4, 3.166e-05,
|
||||
−0.01604}; знаки по квадранту (vmovmskps)
|
||||
out = (e^re·cos(im'), e^re·sin(im')) — БЕЗ КАКОГО-ЛИБО МАСШТАБА ВНУТРИ.
|
||||
```
|
||||
Следствие: аргумент экспоненты в цепи = ровно то, что даёт fwd-RFFT.
|
||||
|
||||
### Нормировки опов (пин по дизасму воркеров)
|
||||
- FWD(1a0c0): сборка X[Nyq]=A0−B0 БЕЗ ×0.5 ⇒ ядро сырое (gain 1 = numpy.rfft);
|
||||
план-масштаб guard `[obj+8]`: у нас [+8]=0 ⇒ ffe0 НЕ вызывается. ИТОГ s_f=1.
|
||||
- INV(4ca80): голова X0/XN бабочка без 0.5, план-масштаб guard `[obj+0xc]`=1
|
||||
⇒ ffe0 ×2^-12 по всему буферу ПОСЛЕ ядра. Ядро сырое (raw IDFT, gain N)
|
||||
⇒ после ffe0: s_i = 1 (ровно numpy.irfft). ИТОГ s_i=1.
|
||||
- Содержимое WINfreq@658 подтверждено по старым дампам (firbufs.npz):
|
||||
периодический Hann(4096), w[1]=5.8827e-07, w[1024]=0.5, w[2047]=0.9999994.
|
||||
|
||||
### Отвергнуто (численно, на ультрачистых кадрах)
|
||||
- Своп направлений (fwd,inv,fwd,inv): rms 82 дБ — исключено.
|
||||
- «Найквост не экспоненцируется»: rms 60 дБ — исключено (EXP накрывает все
|
||||
2049 флоатов включая слот Nyquist.re).
|
||||
- Окно hann^p (p=0.5..4) при q=1: s_F∈[0.81..0.97], не достигает 0.76.
|
||||
- Перестановка («unordered» layout) для кадров класса post-stage: сортировка
|
||||
модулей не совпадает ни с одной стадией (relres 0.28..0.98) — кадры
|
||||
полиморфны, forensics снапшотов исчерпана.
|
||||
|
||||
### ПАРАДОКС q (главный остаток)
|
||||
Все масштабы зафиксированы ⇒ модель обязана быть точной при q=1, однако:
|
||||
q=1 → rms 0.022 дБ; q=0.8 → 0.0065 дБ (медиана, 60 кадров).
|
||||
Более того: наблюдаемый закон cur=trk^γ держится с rms~1e-4 на РАЗНЫХ
|
||||
контентах, а наш сэндвич даёт контент-зависимый наклон s_F (0.82..1.34).
|
||||
⇒ реальный тракт ведёт себя как ПОТОЧЕЧНАЯ степень (диагональный оператор
|
||||
в частотном домене), наш rfft∘fold∘irfft — нет. Гипотеза: ops A–D суть
|
||||
комплексные FFT размера 2048 (не real-4096!) либо содержат zreorder-слой,
|
||||
и срединный линейный оператор L=rfft∘fold∘irfft в действительности близок
|
||||
к диагональному в частотном базисе (например, при интерпретации буфера
|
||||
как 2048 комплексных отсчётов fold-паттерн становится почти тривиальным).
|
||||
|
||||
### Пути закрытия (следующий раунд)
|
||||
1. Статический декод ядер-близнецов (~3.1k строк AVX каждый) — определить
|
||||
размер/тип преобразования и формат укладки окончательно.
|
||||
2. Live: burst-захват rendersnap2 (обновлён, +ACC/WINfreq) с плотным
|
||||
семплированием состояния FIR внутри рендера; сравнить стадии.
|
||||
3. Windows/x64dbg-MCP (если доступен хост): брейкпоинт на 52b644..52b893,
|
||||
дамп FIR после каждого из 6 шагов для одного кадра — закрывает всё.
|
||||
|
||||
## 24mm10-бис: итог статического разбора близнецов (раунд «добить q»)
|
||||
```
|
||||
Ядра 181b853e0(INV-core)/181b81b80(FWD-core): 3130 строк почти развёрнутого
|
||||
AVX2-кода (60 jcc, 4 боевых петли), БЕЗ внутренних вызовов; единственные
|
||||
float-константы ±0.707107 (+ знак-маска 0x80000000) в собственном статике
|
||||
@0x186ee7dxx (в дампе есть). Это radix-4/split-radix КОМПЛЕКСНЫЙ FFT
|
||||
половинного размера (count=N/2=2048 от воркера), специфицированный под
|
||||
размер (jump-table по log2n в воркере выбирает ядро).
|
||||
Нормировок внутри НЕТ (ни одной vmulps на константу ≠±1/√2).
|
||||
INV-воркер добавляет ffe0(2^-12) ([obj+0xc]=1), FWD-воркер НЕ добавляет
|
||||
([obj+8]=0). Твидлы плана лежат инлайн после заголовка плана (захват buf548:
|
||||
квады cos(π/8)-класса @f32-индекс 560+, пары sin|cos дальше);
|
||||
указатели plan[+0x30]/[+0x38]/[+0x50]/[+0x58] — кучные адреса этих таблиц
|
||||
(вне дампа), разность [+0x38]−[+0x30]=0x230Б=140 f32.
|
||||
Инструмент: /tmp/opencode/avx_interp.py (мини-интерпретатор AVX/FMA-
|
||||
подмножества; баги cmp-as-sub и RIP-rel исправлены; довести до прогона —
|
||||
упёрлось в управление потоком на таблицах, см. probe2/probe3.py).
|
||||
```
|
||||
ВЫВОД: пара INV/FWD == пара numpy.irfft/rfft (подтверждено трижды).
|
||||
Парадокс q (модель требует аргумент×0.8, все масштабы зафиксированы)
|
||||
⇒ причина НЕ в нормировках ядер, а в том, ЧТО реально лежит в FIR-буфере
|
||||
в момент γ-кадров: офлайн-гипотезы о стадиях исчерпаны, кадры полиморфны.
|
||||
Необходимо наблюдение ЖИВОГО состояния (варианты 2/3 выше), либо полный
|
||||
декод управления потоком близнеца (петли по стадиям, r13/r14 walk —
|
||||
частично картирован: r13=work, add/sub rbp, add 0x40/0x100).
|
||||
|
||||
### Скорректированные мелочи dataflow этого раунда
|
||||
- Порядок: copy → opA(INV) → FIR[n]=0 → fold×2 → zero[2049..] → opB(FWD)
|
||||
(FIR[n]=0 стоит ПОСЛЕ opA, обнуляет временной отсчёт t[N/2], не вход!)
|
||||
- В модели убрать h[-1]=0 перед irfft (Найквост входа НЕ обнулялся).
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
## ============ 24mm11: LIVE PTRACE-ТРАССИРОВКА WINE-ХОСТА; ЦЕПЬ ДО DF0 БИТ-ТОЧНА; q=1 ============
|
||||
|
||||
### Инструмент (прорыв)
|
||||
scripts/wine_ptrace_trace.py — мини-ptrace отладчик: запускает reaper как
|
||||
ребёнка (yama=1 не мешает), аттачится ко всем тредам wine-хоста yabridge,
|
||||
ставит int3 на входах COPY 1800136e0 / EXP 1803831c0 / DF0 18000b3c0 /
|
||||
DF0RET 52b898, дампит регистры и буферы через /proc/tid/mem между хитами.
|
||||
Контекст НЕ нужен: всё берётся из регистров хитов; кадры сшиваются
|
||||
последовательностью COPY→EXP→DF0→DF0RET.
|
||||
|
||||
### ФАКТЫ (dual_b1q_0.5.rpp, 400+ хитов)
|
||||
1. **Вход EXP == rfft(fold(irfft(scr))) при q=1 ТОЧНО**: отношение
|
||||
Y_meas/Y_model = 1.0000+0.0000j по всем бинам всех кадров. НИКАКОГО
|
||||
скаляра q нет — «q≈0.8» из 24mm9 был артефактом вырожденного фита
|
||||
(гладкая модель подгонялась под гладкую кривую).
|
||||
2. **FIR на входе df0 == полная модель БИТ-В-БИТ**: |ratio|=1.0000,
|
||||
phase=0 по всем 2049 бинам; энергия разностного ядра ~1e-15.
|
||||
Окно = периодический Hann, падающая половина, всё как в модели.
|
||||
3. **df0 = complex-mul подтверждён живьём**: Tout==Tin⊗F с relerr≤3.5e-7
|
||||
(float32).
|
||||
4. **НОВОЕ: track_i ([rsp+0x138]-таблица) ≠ exp(scr)!** Tin — комплексная
|
||||
кривая, СОСРЕДОТОЧЕННАЯ В НОТЧАХ (топ-бины = нотчи скр), |max|~442,
|
||||
наклон log|Tin|/scr ≈ −5.5 ⇒ Tin ≈ trk^5.5; Tout = Tin⊗F ≈ trk^4.54.
|
||||
5. Слоты ctx через сигнатуру vtable нашли ДРУГОЙ инстанс (GUI-класс:
|
||||
слоты содержат 1200/440/25 — частоты/параметры); валидация ctx теперь
|
||||
только по инварианту trk==exp(scr) из хита (строгая: finite, |scr|<40).
|
||||
|
||||
### СЛЕДСТВИЕ: где рождается γ
|
||||
Цепь до df0 бит-точна и НЕ содержит γ. Применённая маска cur@678 =
|
||||
trk^1.760561 образуется ЛИБО самим track_i (выход детекторного каскада
|
||||
шагов 9–17 — ЭТАП B!), ЛИБО пост-df0 нормализацией потребителем
|
||||
(FFT-conv движок 22z). Этап B получил точную цель: декодировать путь
|
||||
lvl_raw → track_i (буфер [rsp+0x138][band]) и пост-обработку до 678.
|
||||
|
||||
### Следующие шаги
|
||||
1. Этап B: каскад lvl_raw→track_i (bandloop_trace, шаги 9–17, ACC@7c8);
|
||||
валидация теперь возможна ЖИВОЙ трассировкой тех же опов (add bps на
|
||||
divide 52d650/acc-dc40/att-rel 1fa0/1940).
|
||||
2. Найти консюмера track/FIR после fn529fe0 (FFT-conv движок) — как
|
||||
track_i превращается в применённую маску аудио.
|
||||
3. C++ порт RT_CASCADE=1 (Этап C) — цепь до df0 уже можно транскрибировать.
|
||||
|
||||
## ============ 24mm12: НАЙДЕН ДЕТЕКТОРНЫЙ КАСКАД = СТАДИЯ vt+0x28 = 180529c60; КАРТА VTABLE ПАЙПЛАЙНА ============
|
||||
|
||||
### Оркестратор (0x1805300f0..0x1805305e7)
|
||||
fn529fe0 вызывается ВИРТУАЛЬНО: `call [ctx_vtable+0x30]` (сайт 530371),
|
||||
аргументы (rcx=ctx, rdx=ТАБЛИЦА TRACK = ctx+0x380, r8d=4096=[ctx+0x1a8],
|
||||
r9d=nbands=[ctx+0x30]). До него в том же оркестраторе — серия виртуальных
|
||||
вызовов других стадий. Полоса-цикл после fn: cmp [rsi+0x30],r13;
|
||||
r15=ctx+0x360, r14=ctx+0x2e0 — ещё таблицы-указатели в малых оффсетах ctx.
|
||||
|
||||
### Карта vtable ctx (live, comb_b1234)
|
||||
```
|
||||
+0x08 0x18052cfb0 +0x10 0x180529550 +0x18 0x1804714b0 (чужой модуль?)
|
||||
+0x20 0x18052b940 +0x28 0x180529c60 ← ДЕТЕКТОР +0x30 0x180529fe0 (fn)
|
||||
+0x38 0x180529ef0 (prep) +0x40 0x18052bbf0
|
||||
+0xe8 0x1804731c0 (чужой модуль?) +0x218 0x180481210
|
||||
```
|
||||
Трасса кадра: [e8,e8]? → 38 → 28(×nbands) → 30(fn). Писец track — ТОЛЬКО
|
||||
vt+0x28 (430/430 изменений md5).
|
||||
|
||||
### 180529c60 — детекторный каскад, ВСЯ функция 0x281 байт
|
||||
```
|
||||
lock bts [ctx+0x2404dc] ; тот же лок что в fn
|
||||
r9 = rdx (СТАРЫЙ track всей таблицы? базовый буфер)
|
||||
r12 = band*2; rcx = [ctx + r12*8 + 0x540678] ; bands_curve полосы
|
||||
n2 = ([ctx+0x64]±)/2+1
|
||||
call 0x1805355d0(bands, rdx, n2) ; ?? первичное смешение
|
||||
loop по [ctx+0x1b0]:
|
||||
rbp = [ctx+r12*8+0x540678]
|
||||
call 0x180530080(rbp, rbp+4, n2-1) ; ??
|
||||
call 0x18052d920(rbp, xmm1=0.5f, n2-1) ; масштаб ×0.5 !
|
||||
r15 = [ctx+0x5406f8] ; vec6f8
|
||||
call 0x1800020f0 / 0x180001850(rsi=rbp+4, rdx=rbp, r8=r15, r9=n2-1)
|
||||
call 0x180001a00 / ...(rcx=r15, rdx=rsi, xmm=0.5f/0.5d)
|
||||
```
|
||||
То есть «шаги 9–17» из BLOCKMAP 24mm5 на самом деле живут ЗДЕСЬ (+хелперы),
|
||||
а НЕ в fn529fe0: fn529fe0 получает ГОТОВЫЕ track-буферы (arg2) и строит
|
||||
из них ядро (df0: T⊗F), не вычисляя детектор!
|
||||
|
||||
### Статус Этапа B
|
||||
Осталось декодировать: хелперы 5355d0 / 530080 / 20f0 / 1850 / 1a00 /
|
||||
52d920-семантику и роль vec6f8@6f8 — десятки инструкций, вся рекуррентия
|
||||
track_{t+1} замкнётся офлайн. Затем γ=1.760561 выводится аналитически.
|
||||
|
||||
### Инструменты раунда
|
||||
scripts/wine_stage_trace.py (vtable-stage трассировщик c md5-атрибуцией
|
||||
записей), scripts/wine_ptrace_trace.py (+TRACKSAVE/DIV/DC40/EXPVAR/FN,
|
||||
mapped-фильтр, строгая валидация ctx по trk==exp(scr)).
|
||||
Грабли: адреса-константы в патчах скрипта молча не применялись при чужом
|
||||
отступе — проверять installed-list; DC40=0x1800dc40 (не 0x18000dc40);
|
||||
DIV/DC40/EXPVAR/FN могут быть вне маппинга процесса (пропускать).
|
||||
|
||||
## ============ 24mm13: ХЕЛПЕРЫ КАСКАДА 529c60 ДЕКОДИРОВАНЫ (статика) ============
|
||||
|
||||
Резолв ILT-цепочек (movsxd rax,[1826159a0]=4; lea r10,tbl; jmp [r10+rax*8]):
|
||||
```
|
||||
5355d0.f → 18e0(ILT) → 0x1800032c0 → call 0x180016140(a,b,n2) [векторная,
|
||||
5-й арг из стека — MXCSR-сохраняющая, тело не дочитано]
|
||||
530080.f → 2090(ILT) → 0x180010e40: b[i] += a[i] IN-PLACE (vaddss;
|
||||
a=rcx, b=rdx, store в rdx!)
|
||||
20f0.f → 0x180011580: dst[i] = a[i] + b[i] (vaddss, dst=r8)
|
||||
1850.d → 0x1800025e0: то же в double
|
||||
1a00.f → 0x180004720: dst[i] = xmm1 · src[i]; skip при xmm1∈{0,1}
|
||||
52d920 : dst[i] *= xmm1 (in-place, skip при 1/0 — как ffe0)
|
||||
```
|
||||
Грабли раунда: адреса теряли ноль при ручном наборе (0x180018e0 vs
|
||||
0x1800018e0) И интермиттент пустые чтения soothe_mem.bin (лечится ретраем).
|
||||
|
||||
### Рекуррентия каскада на полосу (порядок вызовов из 529c60):
|
||||
```
|
||||
A: 16140(bands_curve@678, T_old, n2) ; до цикла, семантика ???
|
||||
цикл по [ctx+0x1b0]:
|
||||
a: bands[1+j] += bands[j] ; j=0..n2-2 (префикс!)
|
||||
b: bands[j] *= 0.5 ; j=0..n2-2
|
||||
c: vec6f8[j] = bands[1+j] + bands[j] ; (20f0, dst=r8=6f8)
|
||||
d: bands[1+j] = 0.5 · vec6f8[j] ; (1a00, xmm1=0.5f/d)
|
||||
→ результат пишется в track-буфер полосы (ctx+0x380 таблица)
|
||||
```
|
||||
Замечание: префикс-аккумуляция (a) + деление пополам (b) + попарное
|
||||
усреднение (c,d) — это вычисление ПОЛУСУММ смежных бинов = построение
|
||||
иерархического сглаживания (wavelet/Haar-класс!) поверх спектра.
|
||||
Tin на захватах ≈ trk^5.5 в нотчах — согласуется с накоплением за много
|
||||
кадров такого экспоненциального смешения.
|
||||
|
||||
### Следующий шаг (финал Этапа B)
|
||||
1. Дочитать 0x180016140 (оп A до цикла — вероятно T_new = α·T_old + β·bands).
|
||||
2. Собрать рекуррентию в numpy, прогнать по 400 живым кадрам
|
||||
(winetrace_casc/chain_samples.pkl: scr→Tin покадрово), подобрать
|
||||
единственные константы из асма (не фитом!), проверить выход == Tin.
|
||||
3. γ=1.760561 затем выводится из замкнутой рекуррентии аналитически.
|
||||
|
||||
### 24mm13-доп: оп A (0x180016140) — начало декодировано
|
||||
Первый внутренний цикл считает ЭНЕРГИЮ КОМПЛЕКСНЫХ ПАР bands-кривой:
|
||||
```
|
||||
for i: E[i] = fma(re,re, im*im), где re=bands[2i], im=bands[2i+1]
|
||||
(denormals→0; MXCSR сохраняется/восстанавливается)
|
||||
```
|
||||
далее в теле — смешение с T_old (константы 0.5/0.5 broad @181c5ce60).
|
||||
⇒ track_i = рекурсивно сглаженная ЭНЕРГИЯ спектра (не амплитуда!) —
|
||||
это объясняет и масштабы (~trk^5.5 после логов), и нотч-концентрацию.
|
||||
Осталось дочитать ~100 инструкций хвоста 16140 (формула смешения с T_old)
|
||||
— рекуррентия замкнётся полностью, γ выводится аналитически.
|
||||
|
||||
### 24mm13-доп2: численная проверка рекуррентии — расхождение, нужен пер-стадийный дамп
|
||||
Симуляция «магнитуды пар → track» НЕ совпадает с захваченным Tin
|
||||
(Tin пикируется в нотчах со значениями ~442, магнитуды там ~0.3).
|
||||
Гипотеза на проверку: между op-A и df0 есть недоучтённый этап, ЛИБО
|
||||
вход каскада — другая кривая (не exp(scr), т.к. scr снят ПОСЛЕ модификаций).
|
||||
Решение с гарантией: добавить в tracer брейкпоинты вход/выход 529c60 и
|
||||
вход/выход 16140 — точные состояния bands/track до и после каждого блока.
|
||||
(Инфраструктура готова, адреса известны; следующий раунд.)
|
||||
|
||||
### 24mm13-доп3: пер-стадийные дампы живьём — op A подтверждён, вход каскада ≠ exp(scr)
|
||||
Трассировщик получил брейкпоинты CIN/COUT (вход/выход 529c60) и
|
||||
AIN/AOUT (вход/выход 16140, возврат 332c). Живые факты (comb_b1234):
|
||||
```
|
||||
AIN: a(rcx)=кривая состояния, ЗНАКОВАЯ, |max|=57.6 (НЕ exp(scr)!)
|
||||
b(rdx)=единичный буфер (init 1.0)
|
||||
AOUT: a — без изменений (16140 вход не трогает)
|
||||
b[i] = |z_i| ТОЧНО (пары a[2i],a[2i+1]) — магнитуды подтверждены
|
||||
CIN/COUT: track между входом и выходом каскада меняется предсказуемо.
|
||||
```
|
||||
⇒ Вход детектора — НАКОПЛЕННОЕ СОСТОЯНИЕ (знаковая кривая с амплитудами
|
||||
до десятков), а не текущий спектр. Полная рекуррентия требует сшить
|
||||
цепочку состояний покадрово — инфраструктура готова (dumps в
|
||||
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, НЕ нулевой при рекуррентности
|
||||
|
||||
@@ -109,8 +109,106 @@ def validate_scr(sim_scr, cap_scr, tol_db=0.05):
|
||||
return float(np.sqrt(np.mean(err ** 2))), int(m.sum())
|
||||
|
||||
|
||||
def win_periodic_hann(N):
|
||||
return 0.5 * (1.0 - np.cos(2.0 * np.pi * np.arange(N) / N))
|
||||
|
||||
|
||||
# ------------------------------------------------ FIR-цепь (24mm9) --------
|
||||
NFRAME = 4096 # n=[ctx+0x540534]
|
||||
NBINS_FIR = NFRAME // 2 + 1
|
||||
Q_EXP = 0.80 # скаляр аргумента EXP; источник в 1803831c0 (ОТКРЫТО)
|
||||
|
||||
|
||||
def winfreq_fall():
|
||||
"""WINfreq@[ctx+0x540658]: периодический Hann(4096), падающая половина."""
|
||||
return win_periodic_hann(NFRAME)[NFRAME // 2:]
|
||||
|
||||
|
||||
def fir_kernel(scr, q=Q_EXP):
|
||||
"""Полная FIR-цепь (BLOCKMAP 24mm9): min-phase кепстральный сэндвич.
|
||||
|
||||
scr(2049) → pack(re=scr,im=0) → FIR[n]=0 (Найквост)
|
||||
→ inv-RFFT → fold(y[1..2047]*=2.0 @1824c41e0; y[2049..4095]=0)
|
||||
→ fwd-RFFT → комплексная EXP (1803831c0, аргумент ×q)
|
||||
→ inv-RFFT → ×падающий Hann → ноль хвоста → fwd-RFFT
|
||||
→ FIR[0]=1, FIR[1]=0. Возвращает |F| (2049).
|
||||
"""
|
||||
h = np.asarray(scr, dtype=np.complex128).copy()
|
||||
h[-1] = 0.0
|
||||
y = np.fft.irfft(h, n=NFRAME)
|
||||
y[1:NFRAME // 2] *= 2.0
|
||||
y[NFRAME // 2 + 1:] = 0.0
|
||||
w = np.fft.irfft(np.exp(q * np.fft.rfft(y, n=NFRAME)), n=NFRAME)
|
||||
w[:NFRAME // 2] *= winfreq_fall()
|
||||
w[NFRAME // 2:] = 0.0
|
||||
F = np.abs(np.fft.rfft(w, n=NFRAME))
|
||||
F[0] = 1.0
|
||||
return F
|
||||
|
||||
|
||||
def mask_from_frame(S, q=Q_EXP):
|
||||
"""mask_sim из слотов кадра: cur ≈ trk · |F(scr)| (df0 complex-mul)."""
|
||||
scr = S[0x540628][:NBINS_FIR].astype(np.float64)
|
||||
trk = S[0x540688][:NBINS_FIR].astype(np.float64)
|
||||
return trk * fir_kernel(scr, q)
|
||||
|
||||
|
||||
def validate_mask_stage(ds, q=Q_EXP, cap=60):
|
||||
"""Валидация масочной ветви на чистых γ-кадрах (24mm9-протокол).
|
||||
|
||||
Отбор: |γ_fit−1.760561|<5e-4 и fit-rms<1e-5 (жёстче pick_clean_frame).
|
||||
Критерий: rms по ВСЕМ 2049 бинам < 0.05 дБ (структурная фаза).
|
||||
"""
|
||||
import glob
|
||||
rmss, gpred = [], []
|
||||
for f in sorted(glob.glob(os.path.join(ds, 'ph*.npz'))):
|
||||
try:
|
||||
d = np.load(f)
|
||||
except Exception:
|
||||
continue
|
||||
if '0x540628' not in d:
|
||||
continue
|
||||
scr = d['0x540628'][:NBINS_FIR].astype(np.float64)
|
||||
trk = d['0x540688'][:NBINS_FIR].astype(np.float64)
|
||||
cur = d['0x540678'][:NBINS_FIR].astype(np.float64)
|
||||
ok = (trk > 1e-30) & (cur > 1e-30) & np.isfinite(scr)
|
||||
if ok.sum() < 50:
|
||||
continue
|
||||
lt, lc = np.log(trk[ok]), np.log(cur[ok])
|
||||
sel = np.abs(lt) > 0.05
|
||||
if sel.sum() < 8:
|
||||
continue
|
||||
g = float(np.sum(lt[sel] * lc[sel]) / np.sum(lt[sel] ** 2))
|
||||
frms = float(np.sqrt(np.mean((lc[sel] - g * lt[sel]) ** 2)))
|
||||
if not (abs(g - GAMMA) < 5e-4 and frms < 1e-5):
|
||||
continue
|
||||
F = fir_kernel(scr, q)
|
||||
lf = np.log(F[sel])
|
||||
lt_s = np.log(trk[sel])
|
||||
sF = float(np.sum(lf * lt_s) / np.sum(lt_s ** 2))
|
||||
gpred.append(1.0 + sF)
|
||||
m = trk * F
|
||||
mm = (cur > 1e-12) & (m > 1e-12)
|
||||
e = (np.log(m[mm]) - np.log(cur[mm])) * 20 / np.log(10)
|
||||
rmss.append(float(np.sqrt(np.mean(e ** 2))))
|
||||
if len(rmss) >= cap:
|
||||
break
|
||||
if not rmss:
|
||||
print('нет ультрачистых кадров в', ds)
|
||||
return
|
||||
rmss = np.array(rmss)
|
||||
print('кадров=%d | rms медиана=%.4f дБ p90=%.4f max=%.4f | '
|
||||
'gamma_pred(1+s_F)=%.6f' %
|
||||
(len(rmss), np.median(rmss), np.percentile(rmss, 90), rmss.max(),
|
||||
float(np.median(gpred))))
|
||||
|
||||
|
||||
def main():
|
||||
import sys
|
||||
if len(sys.argv) > 1 and sys.argv[1] == '--mask':
|
||||
validate_mask_stage(sys.argv[2] if len(sys.argv) > 2
|
||||
else '/tmp/opencode/sc_multi4b')
|
||||
return
|
||||
ds = sys.argv[1] if len(sys.argv) > 1 else '/tmp/opencode/sc_multi6'
|
||||
tract = sys.argv[2] if len(sys.argv) > 2 else '/tmp/opencode/tract_multi6.txt'
|
||||
|
||||
|
||||
@@ -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,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""fir_probe.py — численная реплика FIR-цепи (Этап A3) против захватов.
|
||||
|
||||
Структура по дизасму (BLOCKMAP 24mm6/24mm8 + wrap/worker декод этого раунда):
|
||||
copy: FIR[2j]=scr[j], FIR[2j+1]=0 (18004d900, 2049 пар)
|
||||
opA: th2180 = INVERSE real-FFT (план buf548, N=4096, scale 1/4096)
|
||||
scale: float[1..2047] *= 2.0 ; float[2049..4095] = 0 (52d920/52db50)
|
||||
opB: th1a90 = FORWARD real-FFT
|
||||
EXP: expf in-place по первым 2049 ФЛОАТАМ (140b30, 52b708-716)
|
||||
opC: th2180 = INVERSE
|
||||
window: float[0..2047] *= WINfreq[2048..4095] (52d990, падающий Hann)
|
||||
float[2048..4095] = 0 (52db50)
|
||||
opD: th1a90 = FORWARD
|
||||
fix: FIR[0]=1.0, FIR[1]=0 (52b7cd-e1)
|
||||
df0: track_i := track_i ⊗ FIR (комплексное умножение, 18000b3c0)
|
||||
Цель: воспроизвести cur@678 из trk@688 без свободных параметров.
|
||||
"""
|
||||
import numpy as np
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
||||
NFLOAT = 4098 # 2049 пар
|
||||
NBINS = 2049 # n/2+1, n=[ctx+0x540534]=4096
|
||||
|
||||
|
||||
def load_frame(npz):
|
||||
d = np.load(npz)
|
||||
S = {}
|
||||
for k in d.keys():
|
||||
if k.startswith('0x'):
|
||||
S[int(k[2:], 16)] = d[k]
|
||||
return S
|
||||
|
||||
|
||||
def win_periodic_hann(N):
|
||||
return 0.5 * (1.0 - np.cos(2.0 * np.pi * np.arange(N) / N)).astype(np.float64)
|
||||
|
||||
|
||||
def fir_chain(scr, winfall, variant='flat'):
|
||||
"""scr: 2049 float (log-домен). Возвращает halfcomplex-спектр ядра F[2049]."""
|
||||
# copy/pack: пары (re=scr, im=0) -> inverse rfft вход (numpy: complex[2049])
|
||||
H = scr.astype(np.float64).astype(np.complex128)
|
||||
# opA: inverse real FFT, нормировка 1/N (план scale=2^-12 при активном флаге)
|
||||
y = np.fft.irfft(H, n=4096) # уже содержит деление на 4096
|
||||
# scale/zero по asm: float[1..2047]*=2, float[2049..]=0 (f[2048] не трогаем)
|
||||
y[1:2048] *= 2.0
|
||||
y[2049:] = 0.0
|
||||
# opB: forward
|
||||
Y = np.fft.rfft(y, n=4096) # complex[2049]
|
||||
# EXP по первым 2049 флоатам плоского массива
|
||||
flat = np.empty(NFLOAT)
|
||||
flat[0::2] = Y.real
|
||||
flat[1::2] = Y.imag
|
||||
if variant == 'flat':
|
||||
flat[:2049] = np.exp(flat[:2049])
|
||||
elif variant == 'cplx':
|
||||
Y = np.exp(Y.astype(np.complex128))
|
||||
flat[0::2] = Y.real
|
||||
flat[1::2] = Y.imag
|
||||
Y2 = flat[0::2] + 1j * flat[1::2]
|
||||
# opC: inverse
|
||||
w = np.fft.irfft(Y2, n=4096)
|
||||
# window: float[0..2047] *= падающая половина; хвост = 0
|
||||
w[:2048] *= winfall
|
||||
w[2048:] = 0.0
|
||||
# opD: forward
|
||||
F = np.fft.rfft(w, n=4096)
|
||||
# fix: FIR[0]=1.0, FIR[1]=0
|
||||
F[0] = 1.0 + 0.0j
|
||||
return F
|
||||
|
||||
|
||||
def evaluate(ds, ph_file, verbose=True):
|
||||
S = load_frame(os.path.join(ds, ph_file))
|
||||
scr = S[0x540628][:NBINS].astype(np.float64)
|
||||
trk = S[0x540688][:NBINS].astype(np.float64)
|
||||
cur = S[0x540678][:NBINS].astype(np.float64)
|
||||
# проверка trk == exp(scr)
|
||||
m_ok = trk > 1e-30
|
||||
err_trk = np.abs(np.log(trk[m_ok]) - scr[m_ok]).max()
|
||||
# фит gamma
|
||||
sel = np.abs(scr) > 0.05
|
||||
g = float(np.sum(scr[sel] * np.log(cur[sel])) / np.sum(scr[sel] ** 2))
|
||||
rms_fit = float(np.sqrt(np.mean((np.log(cur[sel]) - g * scr[sel]) ** 2)))
|
||||
winfall = win_periodic_hann(4096)[2048:]
|
||||
out = []
|
||||
for variant in ('flat', 'cplx'):
|
||||
F = fir_chain(scr, winfall, variant)
|
||||
# маска = track ⊗ F (df0), берём реальную часть как применённую маску
|
||||
mask_sim = np.abs(trk * F) if variant == 'cplx' else trk * F.real
|
||||
mm = (cur > 1e-6) & np.isfinite(mask_sim)
|
||||
e_db = 20.0 / np.log(10) * np.log(np.abs(mask_sim[mm])) - \
|
||||
20.0 / np.log(10) * np.log(cur[mm])
|
||||
rms_db = float(np.sqrt(np.mean(e_db ** 2)))
|
||||
out.append((variant, rms_db, int(mm.sum())))
|
||||
if verbose:
|
||||
print(f'{ph_file} [{variant}] gamma_fit={g:.6f} (rms {rms_fit:.1e}) '
|
||||
f'trk_err={err_trk:.2e} MASK rms={rms_db:.4f} дБ / {mm.sum()} бинов')
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
jobs = [
|
||||
('/tmp/opencode/sc_multi4b', 'ph073.npz'),
|
||||
('/tmp/opencode/sc_multi6', 'ph037.npz'),
|
||||
('/tmp/opencode/sc_multi6', 'ph034.npz'),
|
||||
]
|
||||
if len(sys.argv) > 1:
|
||||
jobs = [(os.path.dirname(sys.argv[1]), os.path.basename(sys.argv[1]))]
|
||||
for ds, ph in jobs:
|
||||
evaluate(ds, ph)
|
||||
@@ -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")
|
||||
@@ -20,7 +20,10 @@ import numpy as np
|
||||
|
||||
SLOTS = [0x540668, 0x540548, 0x540550, 0x540598, 0x540628, 0x5406f8,
|
||||
0x540678, 0x540688, 0x5406c8, 0x5406e8, 0x540768,
|
||||
0x540788, 0x5407f8]
|
||||
0x540788, 0x5407f8,
|
||||
# 24mm9: ACC-таблица указателей (шаг 10 combine) и WINfreq
|
||||
# (окно FIR-цепи; падающий Hann — контроль формы)
|
||||
0x5407c8, 0x540658]
|
||||
NARR = 8194
|
||||
SCAL_OFF = 0x540860
|
||||
SCAL_N = 24 # floats -> 0x540860..0x5408c0
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wine_chain_trace.py — живой захват промежуточных состояний FIR-цепи
|
||||
soothe2 через winedbg (wine) + /proc/<pid>/mem.
|
||||
|
||||
Брейкпоинты:
|
||||
EXP 0x1803831c0 комплексная экспонента FIR-цепи (rcx=buf, r8d=count float)
|
||||
DF0 0x18000b3c0 финальный complex-mul (rcx=FIR, rdx=track, r8d=n пар)
|
||||
На хите: читаем rcx/rdx/r8 (info reg), буферы — через /proc/<pid>/mem,
|
||||
копим сэмплы, отпускаем (c). Рендер не убивается.
|
||||
|
||||
Запуск: python3 scripts/wine_chain_trace.py <rpp> [n_hits] [outdir]
|
||||
"""
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
BP_EXP = 0x1803831c0
|
||||
BP_DF0 = 0x18000b3c0
|
||||
CTX_SLOTS = {'scr': 0x540628, 'trk': 0x540688, 'cur': 0x540678,
|
||||
'fir_ptr': 0x540668}
|
||||
|
||||
|
||||
def find_host():
|
||||
import glob
|
||||
for p in glob.glob('/proc/[0-9]*'):
|
||||
pid = int(os.path.basename(p))
|
||||
try:
|
||||
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace')
|
||||
maps = open(f'/proc/{pid}/maps').read()
|
||||
except Exception:
|
||||
continue
|
||||
if 'soothe2' in maps and 'reaper' not in cmd:
|
||||
return pid, cmd[:80]
|
||||
return None, None
|
||||
|
||||
|
||||
def find_ctx(fd, pid):
|
||||
vt = struct.pack('<Q', 0x1824AC210)
|
||||
m48 = struct.pack('<I', 0x47380000)
|
||||
for line in open(f'/proc/{pid}/maps'):
|
||||
parts = line.split()
|
||||
if 'rw' not in parts[1]:
|
||||
continue
|
||||
lo, hi = (int(x, 16) for x in parts[0].split('-'))
|
||||
CH = 16 * 1024 * 1024
|
||||
a = lo
|
||||
while a < hi:
|
||||
n = min(CH, hi - a)
|
||||
try:
|
||||
d = os.pread(fd, n, a)
|
||||
except OSError:
|
||||
break
|
||||
j = d.find(vt)
|
||||
while j >= 0:
|
||||
cand = a + j
|
||||
sb = os.pread(fd, 4, cand + 0x540870)
|
||||
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||
return cand
|
||||
j = d.find(vt, j + 1)
|
||||
j = d.find(m48)
|
||||
while j >= 0:
|
||||
cand = a + j - 0x24
|
||||
try:
|
||||
sb = os.pread(fd, 4, cand + 0x540870)
|
||||
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||
return cand
|
||||
except OSError:
|
||||
pass
|
||||
j = d.find(m48, j + 1)
|
||||
a += n
|
||||
return None
|
||||
|
||||
|
||||
class WineDbg:
|
||||
"""Асинхронный ридер stdout winedbg + обмен командами по приглашению."""
|
||||
|
||||
PROMPT = 'Wine-dbg>'
|
||||
|
||||
def __init__(self, pid):
|
||||
self.p = subprocess.Popen(
|
||||
['winedbg', '--pid', str(pid)],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, text=True, bufsize=1)
|
||||
self.buf = ''
|
||||
self.lock = threading.Lock()
|
||||
self.ev = threading.Event()
|
||||
self.alive = True
|
||||
self.t = threading.Thread(target=self._reader, daemon=True)
|
||||
self.t.start()
|
||||
if not self.ev.wait(30):
|
||||
raise TimeoutError('winedbg не показал приглашение')
|
||||
|
||||
def _reader(self):
|
||||
while self.alive:
|
||||
ch = self.p.stdout.read(1)
|
||||
if not ch:
|
||||
self.alive = False
|
||||
self.ev.set()
|
||||
return
|
||||
with self.lock:
|
||||
self.buf += ch
|
||||
if self.PROMPT in self.buf:
|
||||
self.ev.set()
|
||||
|
||||
def cmd(self, c, timeout=90):
|
||||
with self.lock:
|
||||
self.buf = ''
|
||||
self.ev.clear()
|
||||
self.p.stdin.write(c + '\n')
|
||||
self.p.stdin.flush()
|
||||
if not self.ev.wait(timeout):
|
||||
with self.lock:
|
||||
tail = self.buf[-300:]
|
||||
raise TimeoutError('winedbg timeout после %r; tail=%r' % (c, tail))
|
||||
with self.lock:
|
||||
out = self.buf.replace(self.PROMPT, '').strip()
|
||||
self.buf = ''
|
||||
self.ev.clear()
|
||||
return out
|
||||
|
||||
def close(self):
|
||||
self.alive = False
|
||||
try:
|
||||
self.p.stdin.write('quit\n')
|
||||
self.p.stdin.flush()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
self.p.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def parse_regs(text):
|
||||
regs = {}
|
||||
for mm in re.finditer(r'\b([re]?[a-z]{2,3}|r\d+d?)\s*[:=]\s*([0-9a-fA-F]{4,16})\b', text):
|
||||
name = mm.group(1).lower()
|
||||
val = int(mm.group(2), 16)
|
||||
if name not in regs:
|
||||
regs[name] = val
|
||||
# нормализация имён к 64-битным
|
||||
alias = {'eax': 'rax', 'ecx': 'rcx', 'edx': 'rdx', 'ebx': 'rbx',
|
||||
'esi': 'rsi', 'edi': 'rdi', 'ebp': 'rbp', 'esp': 'rsp'}
|
||||
out = {}
|
||||
for k, v in regs.items():
|
||||
k64 = alias.get(k, k)
|
||||
if k64.startswith('r') and k64.endswith('d') and k64[1:-1].isdigit():
|
||||
k64 = k64[:-1]
|
||||
if len(k64) <= 3 or k64.startswith('r'):
|
||||
out[k64] = v
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||
n_target = int(sys.argv[2]) if len(sys.argv) > 2 else 60
|
||||
outdir = sys.argv[3] if len(sys.argv) > 3 else '/tmp/opencode/winetrace'
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
wav = None
|
||||
for ln in open(rpp, errors='replace'):
|
||||
if 'RENDER_FILE' in ln and '"' in ln:
|
||||
wav = ln.split('"')[1]
|
||||
break
|
||||
if wav and os.path.exists(wav):
|
||||
os.remove(wav)
|
||||
|
||||
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1",
|
||||
shell=True)
|
||||
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
|
||||
'-renderproject', rpp],
|
||||
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
|
||||
t0 = time.time()
|
||||
host = None
|
||||
while time.time() - t0 < 30 and not host:
|
||||
host, cmdl = find_host()
|
||||
if not host:
|
||||
time.sleep(0.002)
|
||||
if not host:
|
||||
print('NO HOST')
|
||||
return 1
|
||||
print('host %d (%s)' % (host, cmdl), flush=True)
|
||||
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||
|
||||
ctx = None
|
||||
while ctx is None and time.time() - t0 < 25:
|
||||
try:
|
||||
os.kill(host, signal.SIGSTOP)
|
||||
except ProcessLookupError:
|
||||
break
|
||||
ctx = find_ctx(fd, host)
|
||||
os.kill(host, signal.SIGCONT)
|
||||
if not ctx:
|
||||
time.sleep(0.005)
|
||||
if not ctx:
|
||||
print('NO CTX')
|
||||
return 1
|
||||
print('ctx %#x' % ctx, flush=True)
|
||||
|
||||
dbg = WineDbg(host)
|
||||
print(dbg.cmd('break *%#x' % BP_EXP)[:160], flush=True)
|
||||
print(dbg.cmd('break *%#x' % BP_DF0)[:160], flush=True)
|
||||
|
||||
def rd(a, n):
|
||||
return os.pread(fd, n, a)
|
||||
|
||||
def rd_f32(a, n):
|
||||
return np.frombuffer(rd(a, 4*n), dtype='<f4').astype(np.float64)
|
||||
|
||||
def rd_q(a):
|
||||
return struct.unpack('<Q', rd(a, 8))[0]
|
||||
|
||||
samples = []
|
||||
hits = {'EXP': 0, 'DF0': 0}
|
||||
t_start = time.time()
|
||||
stall = 0
|
||||
while sum(hits.values()) < n_target and time.time() - t_start < 300:
|
||||
try:
|
||||
out = dbg.cmd('c', timeout=120)
|
||||
except TimeoutError as e:
|
||||
print('timeout:', str(e)[-200:], flush=True)
|
||||
stall += 1
|
||||
if stall >= 3:
|
||||
break
|
||||
continue
|
||||
addrs = [int(x, 16) for x in re.findall(r'0x[0-9a-fA-F]{9,}', out)]
|
||||
pc = None
|
||||
for a in addrs:
|
||||
if abs(a - BP_EXP) < 64:
|
||||
pc = a; kind = 'EXP'; break
|
||||
if abs(a - BP_DF0) < 64:
|
||||
pc = a; kind = 'DF0'; break
|
||||
if pc is None:
|
||||
ir = dbg.cmd('info reg', timeout=30)
|
||||
rr = parse_regs(ir)
|
||||
pc = rr.get('rip', 0)
|
||||
kind = 'EXP' if abs(pc-BP_EXP) < 64 else ('DF0' if abs(pc-BP_DF0) < 64 else None)
|
||||
if kind is None:
|
||||
stall += 1
|
||||
if stall >= 5:
|
||||
print('неопознанные остановки; tail:', out[-200:], flush=True)
|
||||
break
|
||||
continue
|
||||
ir = dbg.cmd('info reg', timeout=30)
|
||||
rr = parse_regs(ir)
|
||||
rcx = rr.get('rcx', 0); rdx = rr.get('rdx', 0); r8 = rr.get('r8', 0)
|
||||
rec = {'kind': kind, 'rip': pc, 'rcx': rcx, 'rdx': rdx, 'r8': r8,
|
||||
't': round(time.time()-t_start, 4)}
|
||||
try:
|
||||
if kind == 'EXP':
|
||||
rec['buf'] = rd_f32(rcx, 4098)
|
||||
rec['count'] = r8
|
||||
else:
|
||||
rec['fir'] = rd_f32(rcx, 4098)
|
||||
if rdx > 0x10000:
|
||||
rec['track'] = rd_f32(rdx, 2049*2)
|
||||
# слоты контекста тем же мгновением (процесс остановлен!)
|
||||
rec['scr'] = rd_f32(ctx+CTX_SLOTS['scr'], 2049)
|
||||
rec['trk'] = rd_f32(ctx+CTX_SLOTS['trk'], 2049)
|
||||
rec['cur'] = rd_f32(ctx+CTX_SLOTS['cur'], 2049)
|
||||
fp = rd_q(ctx+CTX_SLOTS['fir_ptr'])
|
||||
rec['fir_via_ctx'] = rd_f32(fp, 4098)
|
||||
except OSError as e:
|
||||
rec['err'] = str(e)
|
||||
samples.append(rec)
|
||||
hits[kind] += 1
|
||||
if sum(hits.values()) % 10 == 0:
|
||||
print('hits:', hits, flush=True)
|
||||
|
||||
print('сбор завершён:', hits, flush=True)
|
||||
snap_ptrs = {}
|
||||
snap_arr = {}
|
||||
for nm, off in CTX_SLOTS.items():
|
||||
try:
|
||||
p = rd_q(ctx+off)
|
||||
if p > 0x10000:
|
||||
snap_ptrs[nm] = p
|
||||
snap_arr[nm] = rd_f32(p, 4100)
|
||||
except OSError:
|
||||
pass
|
||||
dbg.close()
|
||||
|
||||
with open(os.path.join(outdir, 'chain_samples.pkl'), 'wb') as f:
|
||||
pickle.dump({'samples': samples, 'snap_ptrs': snap_ptrs, 'ctx': ctx}, f)
|
||||
np.savez_compressed(os.path.join(outdir, 'ctx_snap.npz'), **snap_arr)
|
||||
print('saved', len(samples), 'samples ->', outdir, flush=True)
|
||||
for _ in range(600):
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
print('reaper_rc=%s wav=%s' % (proc.poll(),
|
||||
os.path.getsize(wav) if wav and os.path.exists(wav) else 'NONE'), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,648 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wine_ptrace_trace.py — точный пер-оп захват FIR-цепи soothe2 через ptrace.
|
||||
|
||||
Запускает reaper -renderproject как ребёнок (=> ptrace разрешён при любом
|
||||
yama scope), находит wine-хост yabridge (soothe2 в maps), прицепляется ко
|
||||
всем тредам, ставит int3 на входах EXP/DF0 ядра, на хитах читает регистры
|
||||
(PTRACE_GETREGS) и буферы через /proc/tid/mem; между хитами CONT.
|
||||
|
||||
Брейкпоинты:
|
||||
EXP 0x1803831c0 rcx=buf, r8d=count(float)
|
||||
DF0 0x18000b3c0 rcx=FIR, rdx=track, r8d=n(пар)
|
||||
Плюс слоты контекста тем же мгновением (scr/trk/cur/FIR@540668).
|
||||
|
||||
Запуск: python3 scripts/wine_ptrace_trace.py <rpp> [n_hits] [outdir]
|
||||
"""
|
||||
import ctypes
|
||||
import os
|
||||
import pickle
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
BP_EXP = 0x1803831c0
|
||||
BP_DF0 = 0x18000b3c0
|
||||
BP_COPY = 0x1800136e0
|
||||
BP_DF0RET = 0x18052b898
|
||||
BP_TRACKSAVE = 0x18052b574
|
||||
BP_DIV = 0x1803a06a0
|
||||
BP_DC40 = 0x1800dc40
|
||||
BP_EXPVAR = 0x1802dc0e0
|
||||
BP_FN = 0x180529fe0
|
||||
BP_CIN = 0x180529c60
|
||||
BP_COUT = 0x180529ee1
|
||||
BP_AIN = 0x180016140
|
||||
BP_AOUT = 0x18000332c
|
||||
CTX_SLOTS = {'scr': 0x540628, 'trk': 0x540688, 'cur': 0x540678,
|
||||
'fir_ptr': 0x540668}
|
||||
|
||||
libc = ctypes.CDLL('libc.so.6', use_errno=True)
|
||||
PTRACE_ATTACH = 16
|
||||
PTRACE_DETACH = 17
|
||||
PTRACE_CONT = 7
|
||||
PTRACE_SINGLESTEP = 9
|
||||
PTRACE_PEEKDATA = 2
|
||||
PTRACE_POKEDATA = 5
|
||||
PTRACE_GETREGS = 12
|
||||
PTRACE_SETOPTIONS = 0x4200
|
||||
PTRACE_O_TRACECLONE = 1 << 22
|
||||
__WALL = 0x40000000
|
||||
|
||||
libc.ptrace.restype = ctypes.c_long
|
||||
libc.ptrace.argtypes = [ctypes.c_long, ctypes.c_long,
|
||||
ctypes.c_void_p, ctypes.c_void_p]
|
||||
|
||||
|
||||
class UserRegs(ctypes.Structure):
|
||||
_fields_ = [('r15', ctypes.c_uint64), ('r14', ctypes.c_uint64),
|
||||
('r13', ctypes.c_uint64), ('r12', ctypes.c_uint64),
|
||||
('rbp', ctypes.c_uint64), ('rbx', ctypes.c_uint64),
|
||||
('r11', ctypes.c_uint64), ('r10', ctypes.c_uint64),
|
||||
('r9', ctypes.c_uint64), ('r8', ctypes.c_uint64),
|
||||
('rax', ctypes.c_uint64), ('rcx', ctypes.c_uint64),
|
||||
('rdx', ctypes.c_uint64), ('rsi', ctypes.c_uint64),
|
||||
('rdi', ctypes.c_uint64), ('orig_rax', ctypes.c_uint64),
|
||||
('rip', ctypes.c_uint64), ('cs', ctypes.c_uint64),
|
||||
('eflags', ctypes.c_uint64), ('rsp', ctypes.c_uint64),
|
||||
('ss', ctypes.c_uint64),
|
||||
('fs_base', ctypes.c_uint64), ('gs_base', ctypes.c_uint64),
|
||||
('ds', ctypes.c_uint64), ('es', ctypes.c_uint64),
|
||||
('fs', ctypes.c_uint64), ('gs', ctypes.c_uint64)]
|
||||
|
||||
|
||||
def pt(req, pid, addr=0, data=0):
|
||||
if not isinstance(data, int):
|
||||
data = ctypes.cast(data, ctypes.c_void_p)
|
||||
else:
|
||||
data = ctypes.c_void_p(data)
|
||||
return libc.ptrace(req, pid, ctypes.c_void_p(addr), data)
|
||||
|
||||
|
||||
def getregs(tid):
|
||||
r = UserRegs()
|
||||
if pt(PTRACE_GETREGS, tid, 0, ctypes.byref(r)) != 0:
|
||||
raise OSError('GETREGS tid=%d' % tid)
|
||||
return r
|
||||
|
||||
|
||||
def setregs(tid, r):
|
||||
if pt(PTRACE_SETREGS := 13, tid, 0, ctypes.byref(r)) != 0:
|
||||
raise OSError('SETREGS tid=%d' % tid)
|
||||
|
||||
|
||||
def peek(tid, addr):
|
||||
v = pt(PTRACE_PEEKDATA, tid, addr, 0)
|
||||
if v == -1:
|
||||
e = ctypes.get_errno()
|
||||
if e != 0:
|
||||
raise OSError(e)
|
||||
return v & 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
|
||||
def poke(tid, addr, val):
|
||||
if pt(PTRACE_POKEDATA, tid, addr, val) == -1 and ctypes.get_errno():
|
||||
raise OSError('POKEDATA %#x tid=%d: %d' % (addr, tid, ctypes.get_errno()))
|
||||
|
||||
|
||||
def find_host():
|
||||
import glob
|
||||
for p in glob.glob('/proc/[0-9]*'):
|
||||
pid = int(os.path.basename(p))
|
||||
try:
|
||||
cmd = open(f'/proc/{pid}/cmdline', 'rb').read().replace(b'\0', b' ').decode('utf8', 'replace')
|
||||
maps = open(f'/proc/{pid}/maps').read()
|
||||
except Exception:
|
||||
continue
|
||||
if 'soothe2' in maps and 'reaper' not in cmd:
|
||||
return pid
|
||||
return None
|
||||
|
||||
|
||||
def find_ctx(fd, pid):
|
||||
vt = struct.pack('<Q', 0x1824AC210)
|
||||
m48 = struct.pack('<I', 0x47380000)
|
||||
for line in open(f'/proc/{pid}/maps'):
|
||||
parts = line.split()
|
||||
if 'rw' not in parts[1]:
|
||||
continue
|
||||
lo, hi = (int(x, 16) for x in parts[0].split('-'))
|
||||
CH = 16 * 1024 * 1024
|
||||
a = lo
|
||||
while a < hi:
|
||||
n = min(CH, hi - a)
|
||||
try:
|
||||
d = os.pread(fd, n, a)
|
||||
except OSError:
|
||||
break
|
||||
j = d.find(vt)
|
||||
while j >= 0:
|
||||
cand = a + j
|
||||
sb = os.pread(fd, 4, cand + 0x540870)
|
||||
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||
return cand
|
||||
j = d.find(vt, j + 1)
|
||||
j = d.find(m48)
|
||||
while j >= 0:
|
||||
cand = a + j - 0x24
|
||||
try:
|
||||
sb = os.pread(fd, 4, cand + 0x540870)
|
||||
if sb and struct.unpack('<f', sb)[0] > 100:
|
||||
return cand
|
||||
except OSError:
|
||||
pass
|
||||
j = d.find(m48, j + 1)
|
||||
a += n
|
||||
return None
|
||||
|
||||
|
||||
def find_ctx_candidates(fd, pid, fir_ptr):
|
||||
"""Все адреса X (кратные 8), где [X+0x540668]==fir_ptr => кандидат X."""
|
||||
val = struct.pack('<Q', fir_ptr)
|
||||
out = []
|
||||
for line in open(f'/proc/{pid}/maps'):
|
||||
parts = line.split()
|
||||
if 'rw' not in parts[1]:
|
||||
continue
|
||||
lo, hi = (int(x, 16) for x in parts[0].split('-'))
|
||||
CH = 16 * 1024 * 1024
|
||||
a = lo
|
||||
while a < hi:
|
||||
n = min(CH, hi - a)
|
||||
try:
|
||||
d = os.pread(fd, n, a)
|
||||
except OSError:
|
||||
break
|
||||
j = d.find(val)
|
||||
while j >= 0:
|
||||
if j % 8 == 0:
|
||||
out.append(a + j - 0x540668)
|
||||
j = d.find(val, j + 1)
|
||||
a += n
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/dual_b1q_0.5.rpp'
|
||||
n_target = int(sys.argv[2]) if len(sys.argv) > 2 else 80
|
||||
outdir = sys.argv[3] if len(sys.argv) > 3 else '/tmp/opencode/winetrace'
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
wav = None
|
||||
for ln in open(rpp, errors='replace'):
|
||||
if 'RENDER_FILE' in ln and '"' in ln:
|
||||
wav = ln.split('"')[1]
|
||||
break
|
||||
if wav and os.path.exists(wav):
|
||||
os.remove(wav)
|
||||
|
||||
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; "
|
||||
"rm -rf /run/user/1000/yabridge-soothe2_x64-*; sleep 1",
|
||||
shell=True)
|
||||
|
||||
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
|
||||
'-renderproject', rpp],
|
||||
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
|
||||
t0 = time.time()
|
||||
host = None
|
||||
ctx_fd = None
|
||||
ctx = None
|
||||
# Фаза 1: ждём появления хоста и контекста ЧИТАЮЧЕЙ памятью (без ptrace),
|
||||
# чтобы не мешать загрузке плагина
|
||||
while time.time() - t0 < 25:
|
||||
if host is None:
|
||||
host = find_host()
|
||||
if host:
|
||||
try:
|
||||
ctx_fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||
print('host %d (+%.3fs)' % (host, time.time()-t0), flush=True)
|
||||
except OSError:
|
||||
host = None
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
if host is not None:
|
||||
try:
|
||||
ctx = find_ctx(ctx_fd, host)
|
||||
except (ProcessLookupError, OSError):
|
||||
ctx = None
|
||||
host = None
|
||||
time.sleep(0.001)
|
||||
continue
|
||||
if ctx:
|
||||
break
|
||||
time.sleep(0.002)
|
||||
if not host or not ctx:
|
||||
print('NO HOST/CTX (host=%s ctx=%s)' % (host, ctx))
|
||||
return 1
|
||||
print('ctx %#x (+%.3fs)' % (ctx, time.time()-t0), flush=True)
|
||||
fd = ctx_fd
|
||||
|
||||
def rd(a, n):
|
||||
return os.pread(fd, n, a)
|
||||
|
||||
def rd_f32(a, n):
|
||||
return np.frombuffer(rd(a, 4*n), dtype='<f4').astype(np.float64)
|
||||
|
||||
def rd_q(a):
|
||||
return struct.unpack('<Q', rd(a, 8))[0]
|
||||
|
||||
# Фаза 2: аттач ко всем текущим тредам хоста
|
||||
tids = [int(t) for t in os.listdir(f'/proc/{host}/task')]
|
||||
attached = []
|
||||
for tid in tids:
|
||||
try:
|
||||
if pt(PTRACE_ATTACH, tid) == -1 and ctypes.get_errno():
|
||||
raise OSError(ctypes.get_errno())
|
||||
os.waitpid(tid, __WALL)
|
||||
pt(PTRACE_SETOPTIONS, tid, 0, PTRACE_O_TRACECLONE)
|
||||
attached.append(tid)
|
||||
except OSError as e:
|
||||
print('attach fail tid=%d: %s' % (tid, e), flush=True)
|
||||
print('attached %d/%d' % (len(attached), len(tids)), flush=True)
|
||||
|
||||
# Фаза 3: int3 и запуск
|
||||
bps = {}
|
||||
# проверка маппенности по /proc/pid/maps
|
||||
maps_txt = open(f'/proc/{host}/maps').read()
|
||||
|
||||
def mapped(a):
|
||||
for ln in maps_txt.splitlines():
|
||||
rng = ln.split()[0]
|
||||
lo, hi = (int(x, 16) for x in rng.split('-'))
|
||||
if lo <= a < hi:
|
||||
return True
|
||||
return False
|
||||
|
||||
for name, addr in (('COPY', BP_COPY), ('EXP', BP_EXP), ('DF0', BP_DF0),
|
||||
('DF0RET', BP_DF0RET), ('TRACKSAVE', BP_TRACKSAVE),
|
||||
('DIV', BP_DIV), ('DC40', BP_DC40),
|
||||
('EXPVAR', BP_EXPVAR), ('FN', BP_FN),
|
||||
('CIN', BP_CIN), ('COUT', BP_COUT),
|
||||
('AIN', BP_AIN), ('AOUT', BP_AOUT)):
|
||||
if not mapped(addr):
|
||||
print('!! %s@%#x не смапплен — пропуск' % (nm_ := name, addr), flush=True)
|
||||
continue
|
||||
orig = peek(host, addr)
|
||||
poke(host, addr, (orig & ~0xFF) | 0xCC)
|
||||
bps[addr] = (name, orig & 0xFF)
|
||||
print('int3 installed:', {hex(a): n for a, (n, _) in bps.items()}, flush=True)
|
||||
for addr, (nm, _) in bps.items():
|
||||
rb = peek(host, addr) & 0xFF
|
||||
if rb != 0xCC:
|
||||
print('!! %s@%#x НЕ 0xCC: %#02x' % (nm, addr, rb), flush=True)
|
||||
for tid in attached:
|
||||
pt(PTRACE_CONT, tid, 0, 0)
|
||||
|
||||
samples = []
|
||||
hits = {'COPY': 0, 'EXP': 0, 'DF0': 0, 'DF0RET': 0, 'TRACKSAVE': 0,
|
||||
'DIV': 0, 'DC40': 0, 'EXPVAR': 0, 'FN': 0,
|
||||
'CIN': 0, 'COUT': 0, 'AIN': 0, 'AOUT': 0}
|
||||
track_by_tid = {}
|
||||
track_dumps = []
|
||||
regs_by_tid = {}
|
||||
t_start = time.time()
|
||||
|
||||
def snapshot_slots(rec):
|
||||
rec['scr'] = rd_f32(ctx+CTX_SLOTS['scr'], 2049)
|
||||
rec['trk'] = rd_f32(ctx+CTX_SLOTS['trk'], 2049)
|
||||
rec['cur'] = rd_f32(ctx+CTX_SLOTS['cur'], 2049)
|
||||
fp = rd_q(ctx+CTX_SLOTS['fir_ptr'])
|
||||
rec['fir_via_ctx'] = rd_f32(fp, 4098)
|
||||
|
||||
try:
|
||||
while sum(hits.values()) < n_target and time.time() - t_start < 300:
|
||||
try:
|
||||
pid, status = os.waitpid(-1, __WALL | os.WNOHANG)
|
||||
except ChildProcessError:
|
||||
print('нет отслеживаемых процессов', flush=True)
|
||||
break
|
||||
if (pid, status) == (0, 0):
|
||||
# никого не остановлено — короткий сон, дедлайн проверится сверху
|
||||
time.sleep(0.0005)
|
||||
continue
|
||||
if not os.WIFSTOPPED(status):
|
||||
# выход треда/процесса
|
||||
if pid in attached:
|
||||
attached.remove(pid)
|
||||
if pid == host:
|
||||
print('host exited', flush=True)
|
||||
break
|
||||
continue
|
||||
sig = os.WSTOPSIG(status)
|
||||
if sig == signal.SIGTRAP:
|
||||
try:
|
||||
regs = getregs(pid)
|
||||
except OSError:
|
||||
continue
|
||||
site = regs.rip - 1
|
||||
info = bps.get(site)
|
||||
if info is None:
|
||||
# чужой SIGTRAP (clone/event) — просто продолжить
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
kind, obyte = info
|
||||
if kind == 'TRACKSAVE':
|
||||
# rax = track-ptr текущей полосы, r12 = индекс полосы,
|
||||
# [rsp+0x138] = база таблицы указателей (arg2 fn529fe0)
|
||||
tbl = rd_q(regs.rsp + 0x138) if regs.rsp else 0
|
||||
rec_t = {'kind': 'TRACKSAVE', 'tid': pid, 'band': regs.r12,
|
||||
'track_ptr': regs.rax, 'tbl': tbl,
|
||||
't': round(time.time()-t_start, 4)}
|
||||
if len(track_dumps) < 48:
|
||||
try:
|
||||
rec_t['tbl_entries'] = [rd_q(tbl+8*i) for i in range(16)]
|
||||
rec_t['trk_curve'] = rd_f32(regs.rax, 2049*2)
|
||||
except OSError as e:
|
||||
rec_t['err'] = str(e)
|
||||
track_dumps.append(rec_t)
|
||||
samples.append(rec_t)
|
||||
hits['TRACKSAVE'] += 1
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
if kind == 'FN':
|
||||
ra = rd_q(regs.rsp)
|
||||
rec_f = {'kind':'FN','tid':pid,
|
||||
'rcx':regs.rcx,'rdx':regs.rdx,'r8':regs.r8,'r9':regs.r9,
|
||||
'ret':ra,'t':round(time.time()-t_start,4)}
|
||||
samples.append(rec_f); hits['FN'] += 1
|
||||
if hits['FN'] <= 3:
|
||||
print('FN: rcx=%#x rdx=%#x r8=%#x r9=%#x ret=%#x'%(
|
||||
regs.rcx,regs.rdx,regs.r8,regs.r9,ra), flush=True)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
if kind in ('DIV','DC40','EXPVAR'):
|
||||
rec_a = {'kind': kind, 'tid': pid,
|
||||
't': round(time.time()-t_start, 4),
|
||||
'rcx': regs.rcx, 'rdx': regs.rdx,
|
||||
'r8': regs.r8, 'r9': regs.r9}
|
||||
try:
|
||||
for nm, p, cnt in (('a', regs.rcx, 2050),
|
||||
('b', regs.rdx, 2050),
|
||||
('c', regs.r8, 2050)):
|
||||
if p > 0x10000:
|
||||
rec_a[nm] = rd_f32(p, cnt)
|
||||
except OSError as e:
|
||||
rec_a['err'] = str(e)
|
||||
samples.append(rec_a)
|
||||
hits[kind] += 1
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
if kind == 'DF0':
|
||||
track_by_tid[pid] = regs.rdx
|
||||
if kind in ('CIN','COUT'):
|
||||
key='cin_%d'%pid if kind=='CIN' else 'cout_%d'%pid
|
||||
if kind=='CIN':
|
||||
regs_by_tid[pid]=dict(rdx=regs.rdx,r12=regs.r12,
|
||||
rcx=regs.rcx)
|
||||
rec_s={'kind':kind,'tid':pid,'t':round(time.time()-t_start,4)}
|
||||
try:
|
||||
bp=regs_by_tid.get(pid,{})
|
||||
trk=bp.get('rdx',0)
|
||||
if trk>0x10000:
|
||||
rec_s['trk']=rd_f32(trk,4100)
|
||||
# все кривые bands из таблицы ctx+0x540678 (до 4 полос)
|
||||
for bi in range(4):
|
||||
p=rd_q(ctx+0x540678+8*bi)
|
||||
if p>0x10000:
|
||||
rec_s['bands%d'%bi]=rd_f32(p,2050)
|
||||
except OSError as e:
|
||||
rec_s['err']=str(e)
|
||||
samples.append(rec_s); hits[kind]+=1
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
if kind in ('AIN','AOUT'):
|
||||
key='a_%d'%pid
|
||||
if kind=='AIN':
|
||||
regs_by_tid[pid]=dict(rcx=regs.rcx,rdx=regs.rdx)
|
||||
rec_s={'kind':kind,'tid':pid,'t':round(time.time()-t_start,4)}
|
||||
try:
|
||||
bp=regs_by_tid.get(pid,{})
|
||||
for nm,kk in (('a',bp.get('rcx',0)),('b',bp.get('rdx',0))):
|
||||
if kk>0x10000:
|
||||
rec_s[nm]=rd_f32(kk,4100)
|
||||
rec_s['n']=regs.r8&0xFFFFFFFF if kind=='AIN' else None
|
||||
except OSError as e:
|
||||
rec_s['err']=str(e)
|
||||
samples.append(rec_s); hits[kind]+=1
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
if kind == 'DF0RET':
|
||||
tp = track_by_tid.get(pid)
|
||||
rec_r = {'kind': 'DF0RET', 'tid': pid,
|
||||
't': round(time.time()-t_start, 4)}
|
||||
try:
|
||||
if tp and tp > 0x10000:
|
||||
rec_r['track'] = rd_f32(tp, 2049*2)
|
||||
samples.append(rec_r)
|
||||
hits['DF0RET'] += 1
|
||||
except OSError as e:
|
||||
rec_r['err'] = str(e)
|
||||
samples.append(rec_r)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
if kind == 'COPY':
|
||||
try:
|
||||
cnt = min(regs.r9 & 0xFFFFFFFF, 2049)
|
||||
rec_c = {'kind': 'COPY', 'tid': pid,
|
||||
't': round(time.time()-t_start, 4),
|
||||
'src': rd_f32(regs.rcx, cnt),
|
||||
'dst': regs.r8}
|
||||
# снять int3/step/restore как у остальных — общий код ниже
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
samples.append(rec_c)
|
||||
hits['COPY'] += 1
|
||||
continue
|
||||
except OSError as e:
|
||||
print('copy err', e, flush=True)
|
||||
continue
|
||||
# ctx по фактическому указателю FIR из хита + валидация
|
||||
# инварианта trk==exp(scr) (24mm3), строгая
|
||||
if kind == 'DF0':
|
||||
good = None
|
||||
cands = find_ctx_candidates(fd, host, regs.rcx)
|
||||
for cand in cands:
|
||||
if cand <= 0x10000:
|
||||
continue
|
||||
try:
|
||||
v_sc = rd_f32(cand+CTX_SLOTS['scr'], 2049)
|
||||
v_tr = rd_f32(cand+CTX_SLOTS['trk'], 2049)
|
||||
except OSError:
|
||||
continue
|
||||
if not (np.isfinite(v_sc).all() and np.isfinite(v_tr).all()):
|
||||
continue
|
||||
if np.abs(v_sc).max() > 40:
|
||||
continue
|
||||
if np.allclose(v_tr, np.exp(v_sc), rtol=1e-3, atol=1e-9):
|
||||
good = cand
|
||||
break
|
||||
if pc_dbg := True:
|
||||
for cand in cands[:4]:
|
||||
try:
|
||||
vs = rd_f32(cand+CTX_SLOTS['scr'], 2049)
|
||||
vt = rd_f32(cand+CTX_SLOTS['trk'], 2049)
|
||||
except OSError:
|
||||
continue
|
||||
dmax = np.abs(vt-np.exp(np.clip(vs,-80,80))).max()
|
||||
print(' cand %#x: |scr|=%.4g |trk|=%.4g maxdiff=%.4g'
|
||||
% (cand, np.abs(vs).max(), np.abs(vt).max(), dmax),
|
||||
flush=True)
|
||||
print('cands=%d good=%s' % (len(cands), hex(good) if good else '-'),
|
||||
flush=True)
|
||||
if good:
|
||||
ctx = good
|
||||
if kind == 'FN':
|
||||
ra = rd_q(regs.rsp)
|
||||
rec_f = {'kind':'FN','tid':pid,
|
||||
'rcx':regs.rcx,'rdx':regs.rdx,'r8':regs.r8,'r9':regs.r9,
|
||||
'ret':ra,'t':round(time.time()-t_start,4)}
|
||||
samples.append(rec_f); hits['FN'] += 1
|
||||
if hits['FN'] <= 3:
|
||||
print('FN: rcx=%#x rdx=%#x r8=%#x r9=%#x ret=%#x'%(
|
||||
regs.rcx,regs.rdx,regs.r8,regs.r9,ra), flush=True)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
if kind in ('DIV','DC40','EXPVAR'):
|
||||
rec_a = {'kind': kind, 'tid': pid,
|
||||
't': round(time.time()-t_start, 4),
|
||||
'rcx': regs.rcx, 'rdx': regs.rdx,
|
||||
'r8': regs.r8, 'r9': regs.r9}
|
||||
try:
|
||||
for nm, p, cnt in (('a', regs.rcx, 2050),
|
||||
('b', regs.rdx, 2050),
|
||||
('c', regs.r8, 2050)):
|
||||
if p > 0x10000:
|
||||
rec_a[nm] = rd_f32(p, cnt)
|
||||
except OSError as e:
|
||||
rec_a['err'] = str(e)
|
||||
samples.append(rec_a)
|
||||
hits[kind] += 1
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
if kind == 'DF0':
|
||||
track_by_tid[pid] = regs.rdx
|
||||
rec = {'kind': kind, 'tid': pid,
|
||||
'rcx': regs.rcx, 'rdx': regs.rdx, 'r8': regs.r8 & 0xFFFFFFFF,
|
||||
't': round(time.time()-t_start, 4)}
|
||||
try:
|
||||
if kind == 'EXP':
|
||||
rec['buf'] = rd_f32(regs.rcx, 4098)
|
||||
rec['count'] = rec['r8']
|
||||
else:
|
||||
rec['fir'] = rd_f32(regs.rcx, 4098)
|
||||
if regs.rdx > 0x10000:
|
||||
rec['track'] = rd_f32(regs.rdx, 2049*2)
|
||||
if ctx:
|
||||
snapshot_slots(rec)
|
||||
if kind == 'DF0':
|
||||
rec['fir_via_ctx'] = rec.get('fir_via_ctx')
|
||||
except OSError as e:
|
||||
rec['err'] = str(e)
|
||||
samples.append(rec)
|
||||
hits[kind] += 1
|
||||
# снять int3 -> шаг назад -> singlestep -> вернуть int3 -> cont
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
if sum(hits.values()) % 10 == 0:
|
||||
print('hits:', hits, flush=True)
|
||||
elif sig in (signal.SIGSTOP, signal.SIGCHLD, signal.SIGWINCH):
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
else:
|
||||
# посторонний сигнал — доставить
|
||||
pt(PTRACE_CONT, pid, 0, sig)
|
||||
finally:
|
||||
# снять int3 и отсоединиться
|
||||
for addr, (name, obyte) in bps.items():
|
||||
try:
|
||||
poke(host, addr, (peek(host, addr) & ~0xFF) | obyte)
|
||||
except OSError:
|
||||
pass
|
||||
for tid in list(attached):
|
||||
try:
|
||||
pt(PTRACE_DETACH, tid, 0, 0)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
print('сбор завершён:', hits, flush=True)
|
||||
snap_ptrs, snap_arr = {}, {}
|
||||
for nm, off in CTX_SLOTS.items():
|
||||
p = rd_q(ctx+off)
|
||||
if p > 0x10000:
|
||||
snap_ptrs[nm] = p
|
||||
snap_arr[nm] = rd_f32(p, 4100)
|
||||
with open(os.path.join(outdir, 'chain_samples.pkl'), 'wb') as f:
|
||||
pickle.dump({'samples': samples, 'snap_ptrs': snap_ptrs, 'ctx': ctx}, f)
|
||||
np.savez_compressed(os.path.join(outdir, 'ctx_snap.npz'), **snap_arr)
|
||||
print('saved %d -> %s' % (len(samples), outdir), flush=True)
|
||||
for _ in range(600):
|
||||
if proc.poll() is not None:
|
||||
break
|
||||
time.sleep(0.1)
|
||||
print('reaper_rc=%s wav=%s' % (proc.poll(),
|
||||
os.path.getsize(wav) if wav and os.path.exists(wav) else 'NONE'), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wine_stage_trace.py — трассировка СТАДИЙ пайплайна через vtable ctx.
|
||||
|
||||
На входе fn529fe0: читает vtable=[ctx], ставит int3 на таргеты слотов
|
||||
{8,0x18,0x20,0x28,0x30,0x48,0xe8,0x218,0x220,0x228,0x230}, снапшотит
|
||||
track-буферы (таблица @arg2, count=r9). На каждом хите стадии: md5
|
||||
track-буферов + аргументы. Разница md5 между стадиями = кто пишет track.
|
||||
"""
|
||||
import ctypes
|
||||
import hashlib
|
||||
import os
|
||||
import pickle
|
||||
import signal
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
from wine_ptrace_trace import ( # noqa
|
||||
pt, getregs, setregs, peek, poke, find_host, find_ctx,
|
||||
PTRACE_ATTACH, PTRACE_DETACH, PTRACE_CONT, PTRACE_SINGLESTEP,
|
||||
PTRACE_SETOPTIONS, PTRACE_O_TRACECLONE, __WALL)
|
||||
|
||||
BP_FN = 0x180529fe0
|
||||
SLOTS = [0x8, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38, 0x40, 0x48,
|
||||
0xe8, 0x218, 0x220, 0x228, 0x230]
|
||||
|
||||
|
||||
def main():
|
||||
rpp = sys.argv[1] if len(sys.argv) > 1 else '/home/m/soothe-bt/comb_b1234.rpp'
|
||||
n_frames = int(sys.argv[2]) if len(sys.argv) > 2 else 6
|
||||
outdir = sys.argv[3] if len(sys.argv) > 3 else '/tmp/opencode/winetrace_casc'
|
||||
os.makedirs(outdir, exist_ok=True)
|
||||
|
||||
wav = None
|
||||
for ln in open(rpp, errors='replace'):
|
||||
if 'RENDER_FILE' in ln and '"' in ln:
|
||||
wav = ln.split('"')[1]
|
||||
break
|
||||
|
||||
subprocess.run("pkill -9 -x reaper; pkill -9 -f '[y]abridge'; sleep 1",
|
||||
shell=True)
|
||||
proc = subprocess.Popen(['/usr/bin/reaper', '-nosplash', '-ignoreerrors',
|
||||
'-renderproject', rpp],
|
||||
stdout=open('/dev/null', 'w'), stderr=subprocess.STDOUT)
|
||||
t0 = time.time()
|
||||
host = None
|
||||
while time.time() - t0 < 25:
|
||||
host = find_host()
|
||||
if host:
|
||||
break
|
||||
time.sleep(0.001)
|
||||
if not host:
|
||||
print('NO HOST')
|
||||
return 1
|
||||
fd = os.open(f'/proc/{host}/mem', os.O_RDONLY)
|
||||
ctx = None
|
||||
while ctx is None and time.time() - t0 < 25:
|
||||
try:
|
||||
ctx = find_ctx(fd, host)
|
||||
except (ProcessLookupError, OSError):
|
||||
return 1
|
||||
if not ctx:
|
||||
time.sleep(0.002)
|
||||
print('host %d ctx %#x (+%.2fs)' % (host, ctx, time.time()-t0), flush=True)
|
||||
|
||||
def rd(a, n):
|
||||
return os.pread(fd, n, a)
|
||||
|
||||
def rd_f32(a, n):
|
||||
return np.frombuffer(rd(a, 4*n), dtype='<f4').astype(np.float64)
|
||||
|
||||
def rd_q(a):
|
||||
return struct.unpack('<Q', rd(a, 8))[0]
|
||||
|
||||
maps_txt = open(f'/proc/{host}/maps').read()
|
||||
|
||||
def mapped(a):
|
||||
for ln in maps_txt.splitlines():
|
||||
rng = ln.split()[0]
|
||||
lo, hi = (int(x, 16) for x in rng.split('-'))
|
||||
if lo <= a < hi:
|
||||
return True
|
||||
return False
|
||||
|
||||
# attach
|
||||
tids = [int(t) for t in os.listdir(f'/proc/{host}/task')]
|
||||
attached = []
|
||||
for tid in tids:
|
||||
try:
|
||||
if pt(PTRACE_ATTACH, tid) == -1 and ctypes.get_errno():
|
||||
raise OSError(ctypes.get_errno())
|
||||
os.waitpid(tid, __WALL)
|
||||
pt(PTRACE_SETOPTIONS, tid, 0, PTRACE_O_TRACECLONE)
|
||||
attached.append(tid)
|
||||
except OSError:
|
||||
pass
|
||||
print('attached %d' % len(attached), flush=True)
|
||||
|
||||
# vtable + стадии
|
||||
vt = rd_q(ctx)
|
||||
stage_targets = {}
|
||||
for s in SLOTS:
|
||||
tgt = rd_q(vt + s)
|
||||
if mapped(tgt) and tgt not in stage_targets.values():
|
||||
stage_targets[s] = tgt
|
||||
inv = {v: ('vt+%#x' % k) for k, v in stage_targets.items()}
|
||||
print('стадии:', {hex(k): hex(v) for k, v in stage_targets.items()}, flush=True)
|
||||
|
||||
bps = {}
|
||||
for slot, tgt in stage_targets.items():
|
||||
orig = peek(host, tgt)
|
||||
poke(host, tgt, (orig & ~0xFF) | 0xCC)
|
||||
bps[tgt] = (('vt%#x' % slot), orig & 0xFF)
|
||||
orig_fn = peek(host, BP_FN)
|
||||
poke(host, BP_FN, (orig_fn & ~0xFF) | 0xCC)
|
||||
bps[BP_FN] = ('FN', orig_fn & 0xFF)
|
||||
|
||||
for tid in attached:
|
||||
pt(PTRACE_CONT, tid, 0, 0)
|
||||
|
||||
samples = []
|
||||
frames_done = 0
|
||||
cur_frame = None
|
||||
t_start = time.time()
|
||||
|
||||
def track_snapshot(table, nbands):
|
||||
out = {}
|
||||
for i in range(nbands):
|
||||
p = rd_q(table + 8*i)
|
||||
if p > 0x10000:
|
||||
out[i] = hashlib.md5(rd(p, 4098*4)).hexdigest()
|
||||
return out
|
||||
|
||||
try:
|
||||
while frames_done < n_frames and time.time() - t_start < 240:
|
||||
try:
|
||||
pid, status = os.waitpid(-1, __WALL | os.WNOHANG)
|
||||
except ChildProcessError:
|
||||
break
|
||||
if (pid, status) == (0, 0):
|
||||
time.sleep(0.0005)
|
||||
continue
|
||||
if not os.WIFSTOPPED(status):
|
||||
if pid in attached:
|
||||
attached.remove(pid)
|
||||
continue
|
||||
if os.WSTOPSIG(status) != signal.SIGTRAP:
|
||||
pt(PTRACE_CONT, pid, 0, sig if False else 0)
|
||||
continue
|
||||
try:
|
||||
regs = getregs(pid)
|
||||
except OSError:
|
||||
continue
|
||||
site = regs.rip - 1
|
||||
info = bps.get(site)
|
||||
if info is None:
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
continue
|
||||
kind, obyte = info
|
||||
|
||||
def restore_and_go():
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | obyte)
|
||||
regs.rip = site
|
||||
setregs(pid, regs)
|
||||
pt(PTRACE_SINGLESTEP, pid, 0, 0)
|
||||
os.waitpid(pid, __WALL)
|
||||
poke(pid, site, (peek(pid, site) & ~0xFF) | 0xCC)
|
||||
pt(PTRACE_CONT, pid, 0, 0)
|
||||
|
||||
if kind == 'FN':
|
||||
table = regs.rdx
|
||||
nb = regs.r9 & 0xFFFFFFFF
|
||||
cur_frame = {'t': round(time.time()-t_start, 4),
|
||||
'ctx': regs.rcx, 'table': table, 'nbands': nb,
|
||||
'md5_before': track_snapshot(table, nb),
|
||||
'stages': []}
|
||||
rec = dict(kind='FN', **{k: v for k, v in cur_frame.items()
|
||||
if k != 'md5_before'})
|
||||
samples.append(rec)
|
||||
else:
|
||||
if cur_frame is not None:
|
||||
ent = {'stage': kind, 'site': hex(site),
|
||||
'rcx': regs.rcx, 'rdx': regs.rdx,
|
||||
'r8': regs.r8, 'r9': regs.r9,
|
||||
'md5_after': track_snapshot(cur_frame['table'],
|
||||
cur_frame['nbands'])}
|
||||
cur_frame['stages'].append(ent)
|
||||
if kind.startswith('vt') and frames_done < 2:
|
||||
args = {}
|
||||
for nm, p in (('rcx', regs.rcx), ('rdx', regs.rdx),
|
||||
('r8', regs.r8)):
|
||||
if p > 0x10000:
|
||||
try:
|
||||
args[nm] = rd_f32(p, 2050)[:64].tolist()
|
||||
except OSError:
|
||||
pass
|
||||
samples.append({'kind': 'ARG:' + kind, 'site': hex(site),
|
||||
'args64': str(args)[:400]})
|
||||
if kind == 'vt+0x30':
|
||||
# fn529fe0 завершился: финальный md5
|
||||
if cur_frame is not None:
|
||||
cur_frame['md5_after_fn'] = track_snapshot(
|
||||
cur_frame['table'], cur_frame['nbands'])
|
||||
frames_done += 1
|
||||
samples.append({'kind': 'FRAME_END',
|
||||
'frame': cur_frame})
|
||||
cur_frame = None
|
||||
|
||||
restore_and_go()
|
||||
finally:
|
||||
for addr, (nm, obyte) in bps.items():
|
||||
try:
|
||||
poke(host, addr, (peek(host, addr) & ~0xFF) | obyte)
|
||||
except OSError:
|
||||
pass
|
||||
for tid in list(attached):
|
||||
try:
|
||||
pt(PTRACE_DETACH, tid, 0, 0)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
with open(os.path.join(outdir, 'stage_samples.pkl'), 'wb') as f:
|
||||
pickle.dump(samples, f)
|
||||
fr = [s for s in samples if s['kind'] == 'FRAME_END']
|
||||
print('кадров собрано:', len(fr), flush=True)
|
||||
for f_ in fr[:3]:
|
||||
fr_ = f_['frame']
|
||||
print('--- кадр t=%.2f bands=%d' % (fr_['t'], fr_['nbands']))
|
||||
prev = fr_['md5_before']
|
||||
for st in fr_['stages']:
|
||||
ch = '' if st['md5_after'] == prev else ' <<< TRACK ИЗМЕНИЛСЯ'
|
||||
print(' %-8s rcx=%#x rdx=%#x%s' % (st['stage'], st['rcx'],
|
||||
st['rdx'], ch))
|
||||
prev = st['md5_after']
|
||||
print(' после fn:', fr_.get('md5_after_fn'))
|
||||
print('reaper_rc=%s' % proc.poll(), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user