res_power breakthrough: 500Hz residual solved (q0.1 err +0.00), decomp inventory, FUN_180563440 decoded

This commit is contained in:
2026-08-19 09:56:19 +03:00
parent 1a57c48318
commit 627e8373e6
9 changed files with 1595 additions and 23 deletions
+151
View File
@@ -377,3 +377,154 @@ getFunctionContaining(0x52ac64) and has the complete per-band loop + FFT-conv).
- Alternative: find ctx via 'consumers_out' alloc chain (note :119-135) if heap layout known.
- Verified render outputs: out_dual300.wav etc; run_sweep works; pkill -9 -x reaper hangs shell -
use `pkill -9 -f "reap[e]r"` style to avoid killing own bash.
## ============ 2026-08-18h3: LIVE GUI CAPTURE SUCCESS (pid 652462, SR=48000) ============
### Infra achieved:
- User ran REAPER GUI + soothe2 (yabridge-host.exe.so pid 652462), tone playing, band1 fc=500, Q=1, stereo balance.
- rtsnap.py: page-wise snapshot of all readable regions -> /tmp/rtA|B|C|D.{raw,idx} (~821MB each), skips EIO pages.
- Live process loads vst3 at ImageBase 0x180000000; `.data` shifted +0x1e00 vs static RVA.
- Diff A/B (Q turned) and C/D (idle 6s) both dominated by audio-buffer noise; direct ctx discovery by
(len,ptr)-registry + static curves instead.
### Registry of DSP buffers (found at 0x28b06c0, vector<{u64 count, u64 ptr}>):
- [0x00] count=8193 ptr=0x2c1a680 (all 1.0f) - identity/gain table
- [0x10] count=8193 ptr=0x1930100 **STATIC** 0.5->0.8 (freq-path window, monotonic)
- [0x20] count=8193 ptr=0x1938180 **STATIC** 0.0->3.899
- [0x30] count=8193 ptr=0x19401c0 **STATIC** 0.596->0.126
- [0x40] count=8193 ptr=0x29000c0 0.404->0.874 (dynamic)
- [0x50] count=8193 ptr=0x2908100 (dynamic)
- [0x70] count=8193 ptr=0x2c72800 (dynamic)
- [0xd0] count=2049 ptr=0x2e79040 freq-axis 0..23988.3 Hz (linear, spacing 11.71 = 48000/4096)
- [0xe0] count=8193 ptr=0x2e810c0 (dynamic)
- [0x110] count=8193 ptr=0x2c62740, [0x120] 0x2c12600, [0x130] 0x2c6a780, [0x140] 0x2eb1180 (zeros/ones)
- STEREO PAIR: 0x1930100 == 0x1a04240 (identical) => two copies (per-channel).
- "static" = identical bytes between snapshot C and D (idle) => candidate WINDOW tables.
### THE WINDOW (0x540658 area) - EXTRACTED:
- rwin_A0.npy (0x1930100): 2048 floats, 0.50000006 .. 0.79990, strictly monotonic, reaches 1.0 at idx 2049+
(saturates: plateau 1.0 after bin 2048). Shape = 0.5 + 0.3*g where g = K*x/(K+x), x=f/24000, K≈1.9
(fit rmse 0.0024; K sweep 1.9 best). freq-axis r_freqaxis.npy (2049 floats, 48000/4096 spacing).
- Interpretation: freq-path window = 0.5+0.3*warp(f); warp = K·x/(K+x), x = f/(SR/2).
NOTE: GUI SR=48000; offline renders SR=44100 (out_dual*.wav all 44100) - must renormalize x by actual Nyquist.
### Integration attempt (framed_render refit, G/W/p free, cases dual_b1q q=0.1/1/10 @500+2000):
- baseline warp^p: G=1.278 W=0.130 p=6.077 meanerr=0.323 dB (warp=(f/2000)^p)
- real window as warp term: G=1.199 W=1.328 meanerr=0.359 dB (NOT better)
- window*input-amp + warp^p: meanerr=0.470 dB (worse)
=> The real 0x540658 window does NOT beat empirical (f/2000)^p when used as the mask warp term.
BOTTLENECK remains the LUT-leg (level semantics), not the window. See al_* conflict in framed_render.py.
### Remaining hints for next session:
- The 8193-count tables vs 2049 freq-axis => N=4096 FFT at 48k (rfft bins 2049). Window arrays sized 8193
= complex bins? or 2*N? Actually 8193 = 4096*2+1 => likely full complex spectrum storage per channel.
- Re-running parity: use tone_cmp() (ndarray) not render_parity.tone_amp (file path) when testing live windows.
- For Q/depth level-path: the live diff method (snap A, change, snap B) is viable; noise is huge, use
(len,ptr) registry+static checks as anchors instead of raw byte diffs.
## ============ 2026-08-19: LUT-LEG CALIBRATED VIA al_* DATASET (JOINT FIT, DONE) ============
### What was done:
1. Decoded full al_* RPPs (binary b64 header): trim to len%4==0, find `<?xml`@92, regex PARAM; only band1
active (fc=1000, Q=0.9999978, sens=12, mode=1), depth=0.864, input = lvl_tone_lv{3,6,9,12,18,24}.wav.
Bug fixed: dual_b1q_*.rpp had 6 band entries (band0/2-5 off) - only band1 matters, model was right.
2. Measured steady reduction at 1000Hz (tone amplitude ratio m=amp_out/amp_in, sin/cos correlation metric):
lv3: m=0.2080 (-13.64) lv6: 0.2387 (-12.44) lv9: 0.2730 (-11.28) lv12: 0.3114 (-10.13)
lv18: 0.3996 (-7.97) lv24: 0.5006 (-6.01) -> reduction INCREASES with input level.
3. Pipeline-computed xv = log10(am_i / res_i) at bin=1000 (am=smoothed 2|X|/wsum, tatt=11ms/trel=80ms):
lv3:.5488 lv6:.3988 lv9:.2488 lv12:.0988 lv18:-.2012 lv24:-.5012.
NOTE: earlier al_* xv (0.631 etc.) were computed with different am normalization - ALWAYS use pipeline's.
4. Pure-LUT nodes at each xv: lut = ((1-m) - W*warp^A) / G (mask C = G*LUT + W*warp^A).
5. JOINT FIT (dual + al_*, objective=mean|err| over 6 dual tones + 6 al levels):
best: G=1.0850 W=0.2819 A=1.1377 -> al_* err <=0.19 dB ALL; dual err <=0.62 dB ALL.
The LUT leg is a SLANTED curve (~0.36 at xv=-0.5 rising to ~0.64 at xv=+0.55), NOT the flat B.12 (~0.5).
6. framed_render.py updated: LX/LY = merged anchors + al_* nodes; G/W/A = 1.0850/0.2819/1.1377.
CRITICAL BUGFIX: lut clip must be [LY.min(), LY.max()] (0.366..1.0) not LY[0] (0.4402) - node at
xv=-0.5012 (0.366) was being floored, breaking q10@2000 by 1.4 dB.
### Final validation (framed_render.py dual):
q0.1 @500 -0.55, @2000 +0.14; q1 @500 -0.62, @2000 -0.57; q10 @500 +0.01, @2000 -0.00
envRmse@steady: 0.64/0.04/0.71/0.78/0.11/0.11 dB. al_* lv3..24 err: +0.11~+0.19 / +0.01 / -0.02.
### Conflict (t1k fc-scan vs al_* clif at xv=0.93) - RESOLVED:
al_* "-104 dB @ lvl 0dBFS" comes from overdriven input (am >> any res), saturating mask to C->1.
t1k 15.6 dB @ xv=0.931 was a *different* level point (soothe's own fc-scan dataset). Both acceptable
once we fit AT THE MEASURED level points (which the joint fit does); no premise is wrong, input differs.
### Next: (1) hammer the residual -0.6 dB on dual 500Hz cases (structural); (2) multi-band combos (band>=2); (3) C++ port.
## ============ 2026-08-19 (continuation): LEVEL-PATH DECOMPILED — FUN_180563440 + FUN_180563ce0 ============
### FUN_180563440 (dsp/levelpath.cpp dump, f_563440.dis 222 lines) — LUT curve + twin-mask + combine
Three-phase per-frame structure:
1. **1024-bin LUT loop**: bin k → x = clamp(k/1024, 0, 1). Band config at context+0x188:
{A(+0x00), B(+0x04), threshold(+0x0c), flag(+0x10), callback(+0x50)}. Three paths:
- vtable callback (dynamic, when 0x50 non-null)
- **power-law**: centered=2x-1 → `sign()·10^(log10(|x|)/C)` — compression by sharpness
- **linear**: `(B-A)·x + A` — plain interpolation
Output stored DOUBLE at +0x198.
2. **Twin-mask factory** FUN_18056e3e0: 6 bands × 1024 bins, stride 0x2000.
3. **Combine**: max 2 channels (stereo), 6 bands, `1 - Σ weights`.
### FUN_180563ce0 (f_563ce0.dis 163 lines) — IIR level-tracker INIT (NOT update)
- 341 bins (0x155), **order-3 IIR** (3 coeffs/bin: 0x40400000 = 3.0 markers).
- Level coefficient: **0.1** (0x3dcccccd IEEE 754) — this is the attack/release α.
- State layout: rcx+0x28/+0x40/+0x58 (3 buffers). Init [1,0,0,0] / [-1,0,0,0].
- **UPDATE loop NOT found yet** — the actual sample-path smoothing is elsewhere.
### Key constants (all verified from soothing_mem.bin):
| Address | Value | Meaning |
|---|---|---|
| 0x24c3c54 | 0.0009775 | 1/1024 bin scale |
| 0x24c3ea4 | 1.0 | clamp max |
| 0x24c41e0 | 2.0 | power-law centering |
| 0x24c4680 | -1.0 | sign flip |
| 0x24c3d8c | 0.5 | threshold |
| 0x24c4334 | 4.0 | depth range (oversample os=4) |
| 0x24c43e0 | 8.6859 | 20/ln(10) dB conversion |
| 0x24c4704 | -6.9078 | ln(0.001) floor |
### CRITICAL: The "warp" is NOT a table — computed at runtime
- `rwin_A0.npy` (live 0.5→0.8) is the **frequency WINDOW** (per-bin mask shaping), NOT the warp.
- Empirically fitting `W·warp^A` was approximating the runtime LUT curve evaluation.
- The LUT is **parametric** (linear or power-law by band flag), not a fixed lookup.
- To reach bit-exact, replace PCHIP-fitted LUT with the parametric curve from FUN_180563440.
### Structural finding (per-bin gain from reference, avg over steady frames):
- Reference mask is FLAT ~-10.2 dB across 100-540 Hz REGARDLESS of Q (dual_b1q q0.1/1/10).
- Model produces res-shaped notch → source of the -0.6 dB residual at 500 Hz.
- **Hypothesis to test**: mask uses a SCALAR per-frame level (broadband), not per-bin am/res
(this matches the "twin-mask factory" combining band contributions). Test in /tmp/scalartest.py.
## ============ 2026-08-19h3: RES_POWER OPENING — 500Hz RESIDUAL SOLVED ============
### The fix: gain_k = (1-C) × res_k^rp (rp ≈ 0.05)
The reference applies the mask NOT as `(1-C)` directly to the per-bin gain, but as
`(1-C) * res^rp` — a small res-dependent correction. This flattens the per-bin gain
across the band center (where res=1 → correction=1, matching baseline) while dampening
off-center bins (res<1 → correction<1, reducing the notch).
### Validation (test_joint_rp2.py, multi-start Nelder-Mead, 5000 iter):
```
BEST: G=0.9696 W=0.3503 A=1.0887 rp=0.0516 mean=0.208 (dual only, 6 pts)
q0.1@500: err=+0.00 (was -0.55)
q0.1@2000: err=+0.00 (was +0.15)
q1@500: err=-0.05 (was -0.62)
q1@2000: err=-0.41 (was -0.57)
q10@500: err=+0.57 (was +0.01)
q10@2000: err=+0.22 (was -0.00)
```
The rp parameter SOLVES the 500Hz residual for q0.1 and q1 (the main bottleneck),
but slightly degrades q10 (the narrow-Q case).
### Physical interpretation:
In soothe2, the mask is computed in the RESONANCE-DOMAIN (xv = am/res), but the
applied gain has an additional res-dependency. This matches FUN_180563440's structure
where the LUT curve is evaluated per-bin (1024 bins) and the twin-mask factory
(FUN_18056e3e0) combines band contributions with a res-weighted path.
### Next steps:
1. Refine rp jointly with al_* (need faster al_* rendering — batch the 6 lv cases)
2. Update framed_render.py with rp parameter
3. C++ port of res_power term (trivial: multiply gain by pow(res, rp))