docs: archive 4024стр NOTES_LEVEL, единый источник TOTAL README, runbook AGENTS, 3-шаг план

- handoff/NOTES_LEVEL.md 4413→397 (голова 24mm5+), хвост → handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md
- handoff/NOTES_LEVEL_INDEX.md NEW навигатор, handoff/archive/ whitelist .gitignore
- SESSION_HANDOFF/summary → archive + редиректы, NOTES_TWIN/NOTES_CAPTURE сжаты
- README.md:13 единственный источник TOTAL 1.594/dual 0.193, AGENTS runbook, BITEXACT_PLAN 9→3 шага (каскад 9-19/k-маппинг/Δ)
This commit is contained in:
2026-08-29 03:15:42 +03:00
parent 2eb3b690c9
commit f73724fee7
14 changed files with 4732 additions and 5325 deletions
+31 -183
View File
@@ -1,196 +1,44 @@
# RUNTIME CAPTURE — LIVE DSP TABLES (2026-08-19)
# Runtime capture — live DSP tables (registry heartbeat, SR 48000)
## BREAKTHROUGH: heap "registry" object found & read during offline render
- During `reaper -nosplash -renderproject render_long.rpp`, the yabridge-host
allocates a DSP arena containing a **registry array**: a run of `{u64 count, u64 ptr}`
pairs (stride 0x10) pointing at every DSP buffer. At capture time:
registry base = **0x29b06c0** (in anon region 0x2922000, 0x77e000).
(GUI-session registry was 0x28b06c0 — same object, shifted by arena layout.)
- Finding it live: scan host-readable memory (chunked, 8MB) for `u64==8193` (0x2001)
followed by a readable ptr, then require a run of count/ptr pairs at stride 0x10.
- Registry owner chain: 0 hits for a u64==registry address, so it is reached
structurally (object member at some fixed offset), not via an explicit global.
> **Сжато 2026-08-28.** Полный журнал 2026-08-19—2026-08-23 → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md) + [`handoff/NOTES_LEVEL_INDEX.md`](NOTES_LEVEL_INDEX.md). Детали FIR-цепи → `handoff/BLOCKMAP_529fe0.md`.
## Captured tables (SR=44100 project, but internal freq-axis = 48000!)
| reg idx | count | ptr | content |
|---------|-------|----------|---------|
| [00] | 8193 | 0x2a72600 | identity ~1.0 |
| [01] | 8193 | 0x2111140 | **WIN_freq: 0.5 -> 1.0 (saturates; idx1024=0.68, idx2048=0.8)** — the FFT-conv window (0x540658) |
| [02] | 8193 | 0x29f2280 | 0 -> ~0.01 (levels/curve) |
| [03] | 8193 | 0x29fa300 | **0.596 -> 0.126** = matches known rwin_C0 |
| [04] | 8193 | 0x2a02340 | **0.404 -> 0.874** = 1-[03] complement |
| [05] | 8193 | 0x2a0a3c0 | 0.0435 -> ~0 (weight, small) |
| [06] | 8193 | 0x2a12400 | 0.9565 -> ~1.0 (1-[05]) |
| [07] | 8193 | 0x2aca740 | zeros + small negatives |
| [0d] | 2049 | 0x2cd0fc0 | **freq-axis 0..23988.3 Hz, spacing 11.713 = 48000/4096** → internal SR=48000 |
| [0e] | 8193 | 0x2cd9000 | 2.017 -> 0 (LUT/knee?) |
| [0f] | 16384 | 0x2ce9080 | same as [0e] doubled |
| [10] | 16384 | 0x2cf90c0 | 1.2914 -> 0 |
| [12] | 16384 | 0x2a62540 | 11.29.. (scattering) |
| [14]/[15] | 8193 | .. | first-fire IR? 0,0.022,0.104,0.084,0.018,0 |
| [17] | 32768 | 0x2d29140 | 0.9999 -> ~1 (ramp) |
| [19] | 32768 | 0x2d69200 | 1.2915 -> 1.0 |
| others | 32768/65536 | .. | ones / ramps (FFT plans, mirrors) |
## Registry heartbeat
- Key numeric check: registry[03] head 0.5960761, idx1024 0.168, idx2048 0.1257 —
byte-identical to earlier GUI rwin_C0 (0.596 -> 0.126). Confirms registry IS the
authoritative per-bin weight source; tables are stable across sessions/SR.
- **FREQ-AXIS uses internal SR=48000 regardless of project 44100** (spacing 11.713).
This reconciles "rwin tables at 48k" even when rendering 44.1k projects.
Heap run `{u64 count, u64 ptr}` stride 0x10, base `0x29b06c0` (anon 0x2922000) — офлайн, `0x28b06c0` — GUI (сдвиг ареной). Поиск: скан `u64==8193` + readable ptr, требовать run `count/ptr` stride 0x10. Надёжный beacon (vptr/44100-marker — тупик).
## Files saved (handoff/)
- `rtwin_freq_44100.npy` — WIN_freq (8193 f32): 0.5 -> 1.0 (this is live 0x540658 window)
- `rtfreqaxis_48000_internal.npy` — freq-axis (2048 f32, 0..23988.3, spacing 11.713)
- `rtwa_596.npy` — [03] 0.596->0.126
- `rtwb_404.npy` — [04] 0.404->0.874
- `rtwc_043.npy` — [05]
- `rtwd_956.npy` — [06]
- Full raw snapshot: /tmp/snap_all.bin (318MB, entries {lo,sz,bytes}), registry.txt list.
## Captured tables (SR=44100 project → internal freq-axis 48000)
## Method notes (repro)
- rtsnap_fast.py: spawn reaper render_long, find host (soothe2 in maps, not reaper),
sleep 6s (tables built), pread ALL readable maps chunked 8MB -> snap_all.bin.
- pread of large anon regions can EIO -> MUST chunk (8MB); whole-region pread loses data.
- Scan ~0.1s for 318MB once chunked; far cheaper than object-base scan.
- Earlier vptr-based (rtobj/rtdump2/rtall) and 44100-marker scans all failed because
the DSP object has NO static vptr match in a fresh render (host dies / fields live
only during audio) and ctor field +0x24 != 44100 live. The registry run is the
reliable beacon.
| idx | count | content |
|-----|-------|---------|
| [00] | 8193 | identity ~1.0 |
| [01] | 8193 | **WIN_freq 0.5→1.0** (idx1024 0.68, idx2048 0.8) = live `0x540658` |
| [03] | 8193 | **0.596→0.126** = `rwin_C0` (byte-identical GUI) |
| [04] | 8193 | 0.404→0.874 = 1[03] |
| [05] | 8193 | 0.0435→~0 |
| [06] | 8193 | 0.9565→~1.0 |
| [0d] | 2049 | **freq-axis 0..23988.3 Hz, 11.713 = 48000/4096** → internal SR 48000 |
| others | 8193/16384 | опоры, FFT-планы |
## Remaining (for twin IIR attack/release per-bin)
- twin state A/B (342 double per-bin IIR states) still not uniquely located live;
short renders keep them ~0. They are NOT the registry tables.
- Next: render_long + capture at t=10-20s into sustain, then locate the per-bin
attack/release smoothing coefficients (0x540888/88c set, converted via ln(10)/20)
inside the arena near registry.
**Проверка:** `[03]` head 0.5960761 byte-identical GUI — registry authoritative. **Freq-axis spacing 11.713 ⇒ internal SR 48000** независимо от проекта 44100.
## 2026-08-19b (two-point snapshots t1=8s, t2=40s of render_long 180s)
- rtsnap2.py: TWO snapshots of the SAME 180s render at t1/t2; each 318MB/547regs ~0.2s.
- Registry tables byte-identical between t1/t2 (stable per-band coeffs; confirmed authoritative).
- Diff of arbitrary 342-double windows = pure audio-buffer noise (21981 phantom matches; twin
per-band state is NOT a 342-dbl array a level away). Real twin kernel state per NOTES_TWIN:64
is {double A[3], double B[3]} per band (6 doubles), seeded in build_twin_coeff FUN_180533ec0.
- Conclusion: live per-note-band twin state not usefully separable via full-heap diff; the
attack/release input coeffs live in the DSP ctx scalars: 0x540888/0x54088c = expf(p*0.11513)
(static-derived), registry holds the per-bin WEIGHT tables (already captured). Twin kernels
themselves validated statically (twin_check max|err|=1.27e-5). => Step C goal (window + axes +
weights + kernel parity) is effectively CLOSED; only scalar A/R params remain, derived from RPP
params, no live capture needed.
## Files saved
## 2026-08-20 (P1.5: live level-tracker A[] / ctx — CONFIRMED UNREACHABLE)
- rtctx.py (repo): full pipeline = spawn render_long offline → find yabridge-host → chunked
snapshot → registry-scan → ctx discovery (marker base+0x24==40000.0f ∧ base+0x540658==window).
- Two FRESH captures (host live mid-render): registry found (0x28b06c0 GUI / 0x29b06c0 offline,
[00].cnt=8193 identity, [01]/[03] = window/weights). But:
- the window ptr (0x2962??? live) appears as a heap value ONLY inside the registry entry
(0x???6c8 self-field), never as a field of a larger ctx object;
- bases where +0x24==0x472c4400 (40000.0f) exist only in the plugin image (.data/rodata
0x180535d62/0x1805607e5/0x181414341) — inline code constants, no live DSP object.
- => A standalone DSP ctx object with {+0x24==40000, +0x540658==winptr} does NOT exist in the
heap (the DSP object is the registry itself / its buffers are the registry targets).
- CONFIRMS the 2026-08-19b conclusion: per-bin level-tracker A[] (0x4c0528/0x3c0510/0x2c04f8)
and mask scalars are NOT live-separable with the current registry/ctx tools; they are
STATIC/DERIVED from RPP params (0x540888/88c = 10^(att/20), 0x540870 = expf((p·c4348+c44a4)·0.11513),
0x54087c = raw band/mix). Two unknown constants remain from the (lost) binary: 0x24c4348, 0x24c44a4.
- => P1.5 "live capture" is a dead end; scalars must be derived statically or the two missing
constants recovered from the original soothing_mem.bin (not currently present in workspace).
- `handoff/rtwin_freq_44100.npy` — WIN_freq 8193 f32 (live 0x540658)
- `handoff/rtfreqaxis_48000_internal.npy` — 2048 f32
- `handoff/rtwa_596.npy`, `rtwb_404.npy`, `rtwc_043.npy`, `rtwd_956.npy` — [03][06]
- Raw snapshot: `/tmp/snap_all.bin` (318MB, chunked 8MB pread — иначе EIO)
## 2026-08-20c (BREAKTHROUGH: LEVEL-PATH OBJECT captured live in /tmp/snap_rt.bin)
## Method (repro)
The BandConfig A/B/gamma (roadmap gap 2, block of F2/F3) is now LIVE-CAPTURED.
Read-only scan of the existing realtime snapshot `/tmp/snap_rt.bin` (ctx 0x2370040,
render_long.rpp) — no new capture needed.
- `rtsnap_fast.py`: spawn `reaper -renderproject render_long`, find host `soothe2` in maps, sleep 6s, chunked pread ALL → `snap_all.bin`. Scan 0.1s/318MB.
- `rtsnap2.py`: two-point diff — registry byte-identical (stable).
- Live ctx `0x2370040` (`+0x24==48000`, `sens>100`) — единственный populated, остальные empty. `handoff/rtctx_live.json`.
### Method (repro, ~2s)
1. Level-path fingerprint = per-band **level_gain pair buffer**: 0x400 f32 pairs
(`[level, gain]`), with `level[j] == j/1024` exactly (level[0]==0.0, step 1/1024).
Vectorized scan (2nd derivative of level slots == 0 + level[0]==0.0) finds them.
2. Six such buffers at stride 0x2020..0x2040 (band0: 0x4083020, b1: 0x4085040,
b2: 0x4087080, b3: 0x40890a0, b4: 0x408b0e0, b5: 0x408d100).
3. Find u64 refs to the six → consecutive slots stride 0x18 at **+0xe0+band*0x18**
→ object base = **0x3975460** (level-path object).
## Live ctx (realtime playback, `rtctx_live.json`)
### Level-path object (base 0x3975460, region unknown / heap)
- `+0x178` = band-list ptr → 0x32c0c60
- `+0x180` → BandConfig 0x32c0aa0: **A=-24.0, B=+28.0, gamma@0xc=1.0, byte flag@0x10=0** → linear, no callback@0x90
- `+0x188` → BandConfig 0x32c09c8: A=16.0, B=20000.0, gamma@0xc=1.0, flag=0 (freq-range shaped cfg; +0x18.. floats 0.55,7.13,2.77,2.718 = nonlinear shaper consts)
- `+0x4198 + band*0x2000` = **band mask doubles**, 512 usable per band:
band0 ~1.0 const; band1 1.001→1.216 (rising); band2 0.999→0.579 (falling);
band3 1.291→1.002 (falling); band4/5 1.0→~0.983
- `+0xe0+band*0x18` → per-band level_gain pair buffers (live LUT output already has
gains: b0 0.53123 const, b1 0.5314→0.535, b2 const, b3 0.598→, b4 const, ...; many
bands `~0.531` because mask≈1.0 & render_long default cfg)
- Pointers `0x540688` identity, `0x540698` window, `0x5406b8/c8/d8/e8` WA/B/C/D, `0x540748` warp, `0x540758` freqaxis (`v85=995.6 Hz`).
- Scalars: `0x540870=440.955`, `0x540880=25.0`, `0x540884=10.0`, `0x540888/88c=1.0`, `0x54087c=1.0/10.0` (selectivity/sharpness).
- `acc/f6f8` arrays **zero** в стационаре playback — combine idle.
- Bands curve `R=1/mask` peak следует fc (bin43@500 → bin85@1000), `0x5407f8` min 1.0. Применённый фильтр ≠ pointwise копия R (нужен FFT-conv).
### Interpretation / next
- The captured A/B/gamma are the **default render_long config** (A/B semantics =
level-scaler LUT min/max; rendering default band). To get the A/B/gamma of a SPECIFIC
band shape (t1kq_only1_1000 etc.) re-run rtctx_rt.py with that test RPP and re-scan
the same fingerprint (base offset shifts). Method is now automated.
- This UNBLOCKS the parametric band-LUT 0x563440/0x563a60 as a structural source
(t=(xA)/(BA), clamped, ^gamma, ×norm) instead of fitted Pchip.
- Dump helper: /tmp/dump_levelpath.py, /tmp/probe_base.py.
The dead end above was wrong — the missing piece was REALTIME audio playback, not more scanning.
`-renderproject` uses the OFFLINE audio engine (fields live "only during audio", per earlier note);
the ctx object only materializes during a realtime transport play.
### Method (works)
- play.lua (repo): `reaper.Main_OnCommand(1007)` (Transport:Play) + hold ~300s.
- rtctx_rt.py (repo): `reaper render_long.rpp play.lua` → find yabridge-host → chunked snapshot
(~796MB, 1056 regs) DURING playback → scan heap for the ctx.
- ctx marker that WORKS live: **+0x24 == 48000.0f (0x473b8000)**, NOT 40000.0f (40000 was the
static ctor rodata value; live it is the internal SR = 48000, confirming NOTES_CAPTURE SR=48000).
### Result (captured live, saved handoff/rtctx_live.json)
- **ctx = 0x2370040** (region 0x2022000). Field pointers (all point at registry tables):
0x540688=identity([00] 0x2962580), 0x540698=window([01] 0x14b4240), 0x5406a8=levels([02]),
0x5406b8=WA([03]), 0x5406c8=WB([04]), 0x5406d8=WC([05]), 0x5406e8=WD([06]),
0x540748=warp([12] 0x14bc280), 0x540768=LUT-knee([14]). (NOTE: offsets +0x40 from the
earlier static table — window is 0x540698 live, not 0x540658 as in the f_52b570 disasm label.)
- **Mask scalars (float)**: 0x540870=440.955 (sens), 0x540874=1.0, 0x540878=1.0, 0x54087c=1.0,
0x540880=25.0, 0x540884=10.0, 0x540888=1.0 (attack=10^0), 0x54088c=1.0 (release=10^0),
0x540890=0, 0x540894=1200.0.
- **level-tracker A[] (341 double each, per-bin IIR attack/release coeffs)**:
- 0x4c0528 (attack): 0 → 0.340, 0.348, ... monotonic rising, plateau 0.6921 @bin>=319.
- 0x3c0510 == 0x2c04f8 (release): 0 → 0.000753, 0.000885, ... slow small rise.
Full arrays in handoff/rtctx_live.json (keys A_4c0528, A_3c0510, A_2c04f8).
- BandConfig @ctx+0x188 is NOT populated here (zeros) — it lives at a different offset or only
during band processing; still TBD (but LUT curve params A/B/γ are RPP-derived per roadmap).
- Two more ctx-like bases found (0x2120040, 0x1780040) also have +0x24==48000; 0x2370040 is the
populated one (0x2120040 has 0x540658..698 = 1.0 fill pattern — likely a second/free instance).
## 2026-08-23 (22y): LIVE CTX CAPTURE DURING DUAL/T1KQ/RES PLAYBACK — scripts/dualtrace.py
Method works reproducibly: reaper <cfg>.rpp play_loop.lua (repeat ON) → yabridge-host
→ chunked snapshot ×2 → ctx marker +0x24==48000 ∧ sens>100 → ONLY ONE populated base
(0x2370040; others empty instances). Snapshots byte-stable over seconds.
### Pointer-table catalog (ctx+0x540600..0x540a00, dereferenced u64 → f32[2049])
- Static weights confirmed live: [00]identity@0x540688(ones), window@0x540698(0.5→0.8),
WA/WB/WC/WD @0x5406b8/c8/d8/e8, warp@0x540748(1.3→6.68, structure at LOW bins),
freqaxis@0x540758(v85=995.6Hz ✓ internal 48k/4096).
- **acc/f6f8 arrays ALL ZERO during steady looped playback** (0x5406f8,
0x5407a8/b8/c8/d8 — zero as f32 AND f64): combine accumulators idle in steady state.
- **CONFIG-DEPENDENT CURVE FAMILY** (peak follows band fc: bin43@fc500 → bin85@fc1000):
- 0x540768 == 0x540778 (identical twins): smooth curve, peak at center
(dual: 4.15@43, valley 1.60@171, upturn 1.86@400; t1kq: 3.55@85).
- 0x540788: sharper version (floor ~0.52-1.0, max 4.38).
- **0x5407f8: min EXACTLY 1.0 → reduction multiplier R(f)=1/mask ≥ 1**
(res500 cfg: R(500Hz)=12.0 dB, falls to ~0 by 6 kHz; notch-shaped ✓).
- bands[] slots from static asm (@0x540678+i·16) read as identity/ones tables LIVE —
the per-band working data is NOT sitting in those ctx fields during playback.
### Decisive mismatch
For dual cfg: R(43)=3.98→12.0 dB (real 10.32 ok-ish) BUT R(171)=1.41→3.0 dB while
real cut@2000 = 11.82 dB. ⇒ Applied filter ≠ pointwise copy of R: massive spectral
coupling between template and actual filtering. Prime suspect: FFT-conv stage with
the 8193-wide WIN_freq window ([01]) — smearing/spreading step completely absent in
our pointwise render48k path. This ALSO explains why faithful v1 (pointwise) cannot
balance dual tones regardless of law constants.
### Caveats / next
- Quick Welch TF estimate unreliable (window/alignment) — Goertzel-at-tones stays canon;
for full-spectrum truth use chirp/two-tone refs or per-fc capture sweep.
- NEXT: (1) fc-scan captures (res_only1_{fc}.rpp, 11×) → correlate R_cap(bin85) with
real cut@1000 across fc — validates R as THE applied curve; (2) decode the FFT-conv
0x535a70 body + WIN_freq usage — reconstruct mask→FIR spreading; (3) re-check whether
0x540768-family updates frame-by-frame (two-point diff showed stable — maybe only
rebuilt on param change / note onset).
Дальше — `handoff/NOTES_LEVEL.md:24mm11+` (live ptrace) + `handoff/BLOCKMAP_529fe0.md`.
+5 -4021
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
# NOTES_LEVEL — оглавление журнала
> **Навигатор:** `NOTES_LEVEL.md` — живая голова (24mm5+, 2026-08-28, 389 строк). Архив 2026-08-18—2026-08-23 → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md) (4024 строки). Статус TOTAL → [`README.md:13`](../README.md).
## Живая голова (`handoff/NOTES_LEVEL.md`)
| Дата | Заголовок | Суть |
|------|-----------|------|
| 24mm524mm7 | Полная буферная карта, DESIGN 1802a24c0 | Два круга log→exp, bidir×2 в лог-домене |
| 24mm8-бис | Точка входа следующего раунда | opB 18004ca80, поиск γ |
| 24mm9 | FIR-цепь MIN-PHASE, 0.0065 dB | Цепь 52b60c+, RFFT близнецы, df0 complex-mul, валидация |
| 24mm10 | EXP ядро полностью, парадокс q | Формула 1803831c0, нормировки s_f/s_i=1 |
| 24mm10-бис | Итог близнецов radix-4 | INV/FWD 2048, твидлы 548, без масштабов |
| 24mm11 | LIVE ptrace, цепь до df0 бит-точна | wine_ptrace_trace, Y_model=1.0, df0 complex-mul |
| 24mm12 | Детекторный каскад vt+0x28=180529c60 | Оркестратор 5300f0, vtable карта, 0x281 байт |
| 24mm13 (+доп) | Хелперы каскада 529c60 | 5355d0→16140, 530080, 20f0/1850/1a00, рекуррентия Haar |
| 24mm14 | Каскад декодирован — 3 фазы | `|z|` → Haar×2 → peak/sin/w/blend 5407a8 |
## Архив (`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`) — по периодам
| Период | Ключевые темы | Где |
|--------|---------------|-----|
| 2026-08-18 | Две twin-цепочки, FUN_180563440/563ce0, 530d30 веса, FUN_180529fe0 consumer, 0x530d30 NEGATIVE, bridge B.12 | строки 1300 |
| 2026-08-18h2h3 | Runtime capture, поле 0x540658, registry 0x28b06c0/0x29b06c0, window 0.5→0.8, freq-axis 48000 | 475580 |
| 2026-08-19 | LUT-leg joint fit, level-tracker, decomp lock, registry heartbeat, P4 mask-chain, SR mismatch | 580900 |
| 2026-08-20ix | P2 kernels 8d60/3c40, twin-mask, F0 gate determinism, parametric LUT benches, wrappers | 9001100 |
| 2026-08-21bd | IIR3 bidir, combine scope, structural corpus 2.286, reduction-law affine, Phase A | 11001360 |
| 2026-08-22af | Phase B refuted, Step7 GUI-only, param bridge, floor 20.72 blend·ln10/20 | 13601600 |
| 2026-08-22gp | NO-LUT, white-noise probe, bare chain, pool/scale/floor, joint-fit, dual diag, affine port | 16001970 |
| 2026-08-23a–z | Скалярный закон насыщен, 529fe0 по raw asm, faithful-цепь, теорема α, спрединг отвергнут | 1970–2300 |
| 24j–24ab | Применение декодировано: A=1.019·V^1.8345, FIR=exp(scratch), двухстадийность γ₀=1.79 | 27503550 |
| 24bb24kk3 | k-маппинг, q-независимость, далёкий тон локально, дистанционная серия, симулятор не замкнулся | 3550–4000 |
| 24ll24mm4 | multi6, g_k пер-пиковый, archive закрытие, γ=1.760561, точ. законы отвергнуты | 4000–4024 |
## Как читать
1. **Старт:** `README.md:13` (статус) → `AGENTS.md` (runbook) → `BITEXACT_PLAN.md:1` (3 шага) → этот индекс → `handoff/BLOCKMAP_529fe0.md`.
2. **Детали по теме:** `rg -n "24mm9\|Q-НЕЗАВИСИМОСТЬ\|Haar"` в соответствующем файле.
3. **Полный поиск:** `rg -n "cut_D\|lvl_raw\|5407c8" handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md handoff/NOTES_LEVEL.md`.
## Дубли устранены
- `UPDATE 2026-08-22g` дубль (строки 1577/1591 в архиве) — логически один.
- `UPDATE 2026-08-20s` дубль (974/977) — один.
- Заголовки унифицированы: `## ============ UPDATE``##` в живой голове; архив сохранён как есть для истории.
+23 -70
View File
@@ -1,76 +1,29 @@
# Twin FUN_180535880 transcription (Phase 2)
# Twin FUN_180535880 — краткая справка (детали → `dsp/twin.cpp`)
## PHASE-3 CALLER + GRID (2026-08-18)
- Caller FUN_180536300 (decomp 6218 / sp_180536300.txt) decoded at buffer level:
- params: param_1=top DSP, param_2=out, param_3=?, param_4=band obj, param_5=cell count,
param_7=scratch (>=52*N bytes complex).
- scratch: +0..8N A-accum, +8N..16N B-accum, +16N..24N z-powers, +24N..32N rotor-in
(fill 1.0), +32N..36N phase ramp lVar1, +36N..44N rotor-out z1 (r14), +44N..52N
lVar3(1.0)/lVar4(0.0) for interleave+negate+exp build.
- 0x18052da00(lVar1, scale=2pi/(iVar7*sr), N) = ramp fill (float, via 0x180001a00);
- twin always = FUN_180535880 (type-info compare of same addr -> equal);
sibling FUN_180536f90 is dead.
- 0x18052dbc0(param_2, lVar4, N) = cplx interleave -> out=(cos,sin)=e^{+i theta}.
- GRID LAW (verified from trace.txt case8 + constructor):
- os = *(int*)(param_1+0x240080) = 4 (written 0x18052ce3e `mov ...,0x4`)
- sr = *(float*)(param_1+0x24) = 44100.0
- fs_total (generator) = os*sr = 176400 => resonance shape f/fc invariant to fs.
- per-cell phase step = 2*pi/(os*sr); cell k <-> freq = k/os Hz (0.25 Hz steps).
- Phase-1 model FS=44100 was a valid normalization (res(f) depends only on f/fc).
- case8 generator call (trace 33361..33490, switchD_1805318ab):
- +0x814 numBands = 1, +0x103 mode flag = 1
- param_5 (float, xmm4) = 10^(sens_dB/20) (pow10 10.0^([band+0x101]/20.0))
- gain = sqrtf(param_5) = 10^(sens_dB/40); model fit gain=4.132 => sens_dB@0x101 ~= 24.65
- freq = [band+0x804], Q = [band+0x80c], fs_total = os*sr (w0 = max(fc,2)*2pi/fs_total)
> **Статус:** транскрибирован, gate PASS `dsp/build/twin_check` max rel err 1.27e-5 (float-parity). Полный журнал 2026-08-18 → [`handoff/archive/NOTES_LEVEL_2026-08-18_2026-08-23.md`](archive/NOTES_LEVEL_2026-08-18_2026-08-23.md) + [`handoff/NOTES_LEVEL_INDEX.md`](NOTES_LEVEL_INDEX.md).
## PHASE-2 GATE: PASSED (2026-08-18)
- `dsp/twin.hpp` + `dsp/twin.cpp` written: build_twin_coeff (FUN_180533ec0 double pipeline
-> cvtpd2ps), cplx_div_exact (0x181a77520: mulps |A|^2, rcpps + Newton, NaN guard),
cplx_mul_exact (0x18000ad60 scalar body @0x18000ae00: vfmaddsub213ps form),
twin_apply (seed A0/B0 + 2 vfmadd213ss Horner stages + 2*div).
- twin_check gate: fc-scan grid (7 fc, Q=0.9999978, gain=4.132, tone 1000 Hz)
max |rel err| = 1.27e-5 vs double reference res_at (float rcpps parity).
DUAL sweep (Q=0.1..10 x tones 500/2000, fc=500) max = 1.23e-5. Both PASS.
- Constants locked: A1 = B1 = cos(w0)*-2.0, z1 = conj(exp(+i*theta)),
z2 = cplx_mul(z1,z1), out = 2*B/A.
## PHASE-3 Caller + Grid
## Confirmed scalar math (from generator + cplx-div kernels)
- Generator FUN_180533ec0 (per band, case8):
`fVar1 = sqrtf(param_5)` (param_5 = pow(10, sens/20) via cd6 IAT stub)
`w0 = max(2.0, freq)*2*pi / fs_total` (fs_total = 2*FS frame?)
`p = sin(w0)*0.5 / Q`
`dVar5 = p*fVar1` (A tap), `param_4 = p/fVar1` (B tap)
A = [1+dVar5, dVar2, 1-dVar5], B = [1+param_4, dVar2, 1-param_4]
where dVar2 = cos(w0) * DAT_1824c46b0 (=-2.0? VERIFY)
- Twin FUN_180535880: for each FFT bin z (rotor):
`res(z) = 2 * B(z) / A(z)`, B(z)=B0+B1*z+B2*z^2, A(z)=A0+A1*z+A2*z^2
implemented as complex FMA accumulation over 3 taps:
r15 (A-accum) and r12 (B-accum) via 0x180001fd0 vfmadd213ss (coeff [rsp+k*4+0x28]=A_k, [+0x38]=B_k)
rbp buffer = z powers via rotor ops (0x1800019a0 fill / 0x1800021e0 / 0x1800018b0 / 0x180001af0 / 0x180001dc0)
final 0x180001e20 -> 0x181a775a0 cplx-div: out = 2*B/A (xmm7=0x40000000=2.0, xmm6=NaN guard)
- cplx-div kernel 0x181a77520: |A|^2 via shufps 0x88/0xdd, rcpps + 1 Newton step (xmm7=2.0), NaN guard via cmpeqps.
Matches rotor_kernel.hpp cplx-div exactly.
- Caller `FUN_180536300` (6218): scratch 52·N, `0x18052da00` ramp `2π/(os·sr)`, `0x18052dbc0` interleave `e^{+iθ}`. Twin всегда `180535880`, sibling `180536f90` dead.
- Grid: `os=4` (`+0x240080`), `sr=44100` (`+0x24`), `fs_total=176400`, `phase=2π/176400`, cell `k ↔ k/4 Hz` (0.25 Hz).
- case8: `+0x814=1`, `+0x103=1`, `param_5=10^(sens/20)`, `gain=√param_5=10^(sens/40)` → fit 4.132 ⇒ `sens≈24.65 dB`.
## Dispatch (all stubs share global 0x1826159a0 = 4 = AVX2)
0x180001d00 -> 0x180009860 -> 0x180040d40 (cplx fill const, NT stores)
0x1800019a0 -> 0x180004200 (float fill -> 0x181a63fe0)
0x1800021e0 -> 0x1800130e0 (large AVX, 0x450 stack, rotor/exp?)
0x1800018b0 -> 0x180003040 (-> 0x181a557c0)
0x180001af0 -> 0x180005a20 (loop -> 0x18001a5a0)
0x180001dc0 -> 0x18000ad60 (cplx op r14*rbp)
0x180001e20 -> 0x18000b820 (-> 0x181a775a0 cplx-div 2B/A)
0x180001fd0 -> 0x18000f620 (vfmadd213ss: out = src*coeff + dst)
## PHASE-2 Gate
## Coefficient layout in twin (param_3 = {double A[3], double B[3]})
stack: +0x28=A0 +0x2c=A1 +0x30=A2 | +0x38=B0 +0x3c=B1 +0x40=B2
loop k=0..2 uses A_k via [rsp+k*4+0x28], B_k via [rsp+k*4+0x38]
r15 = out A-accum, r12 = out B-accum, rbp = z^1..z^2 buffer, r14 = rotor(z) input
- `dsp/twin.cpp`: `build_twin_coeff` (FUN_180533ec0), `cplx_div_exact` (rcpps+Newton), `cplx_mul_exact` (vfmaddsub), `twin_apply` `2·B/A`.
- Gate: fc-scan 7fc + dual Q0.1..10 PASS.
## PHASE-1 LOCK (empirical, 36 points, LUT chain B.11)
Effective sqrtf(param_5) needed ~= 4.13 (NOT 1.995).
=> param_5 ~= 17.07 = 10^(1.2325) => internal sens_stored ~= 24.65 dB (not XML 12.0)
OR equivalent. RULED OUT: sqrt(10^(12/20))=1.995 (rmse 1.38/5.68);
10^(12/20)=3.981 direct (t1kq 0.056 but t1k 0.934); |2B/A|^2 (1.5/6.6).
Sens setters (0x18053795d, 0x18056f002) both store RAW value; +0x808 scaling is HOST-side
(plugin controller, outside dump) => must be resolved via bit-exact render or host trace.
NOTE: single-gain compromise 4.132; t1kq wants 4.055, t1k wants 4.137 (tilt residual).
## Scalar math
- Generator: `fVar1=√param_5`, `w0=max(2,freq)·2π/fs_total`, `p=sin(w0)·0.5/Q`, `A=[1+p·fVar1, 2cos w0, 1p·fVar1]`, `B=[1+p/fVar1, 2cos w0, 1p/fVar1]`.
- Twin: `res(z)=2·B(z)/A(z)`, Horner `0x180001fd0`, `cplx-div 0x181a77520`, `z2=z1·z1`.
## Dispatch
Стабы → `0x1826159a0=4` (AVX2). Layout `param_3={A[3],B[3]}` `+0x28/+0x38`, coeff `[rsp+k*4]`.
## PHASE-1 lock
`√param_5≈4.13` (не 1.995), `sens≈24.65 dB` (host ×2 над XML 12.0). Setter `+0x808` host-side.
Детали декомпа — `handoff/nls_dasm/twin*.dis`, `dsp/twin.cpp:27`.
+6 -254
View File
@@ -1,256 +1,8 @@
# SESSION HANDOFF — soothe2 detector reverse → bit-exact render
# SESSION_HANDOFF — перенесён в архив
Prepared: 2026-08-18 (checkpoint end-of-session: commits c8f97e4 + 60bf3a2 pushed). Start: READ THIS FIRST.
> **Исторический чекпоинт 2026-08-18 (коммиты c8f97e4+60bf3a2).**
> Актуально: декомп DSP-ядра ~95% закрыт, `FUN_180529fe0` mono-path, `FUN_180563440/563ce0/563a60` расшифрованы, дизассемблы в `handoff/nls_dasm/` (134 файла).
> Полный текст сохранён: [`handoff/archive/SESSION_HANDOFF_2026-08-18.md`](archive/SESSION_HANDOFF_2026-08-18.md) (256 строк, инвентарь декомпа §0, ключевые адреса §2, Phase-5 план §6).
> Навигация → [`handoff/NOTES_LEVEL_INDEX.md`](NOTES_LEVEL_INDEX.md) + [`handoff/BLOCKMAP_529fe0.md`](BLOCKMAP_529fe0.md) + [`README.md:13`](../README.md).
> **2026-08-20 UPDATE**: см. актуальный канон — `AGENTS.md` и `handoff/NOTES_LEVEL.md`.
> Нижеследующее «NOT DECODED»/«missing body» УСТАРЕЛО: `FUN_180529fe0` mono-path,
> `FUN_180563440/563ce0/563a60` — расшифрованы, полные дизассемблы скопированы в
> `handoff/nls_dasm/` (134 файла). Этот файл — исторический чекпоинт.
## 0. DECOMPILATION INVENTORY (2026-08-19 — what's decoded, where, and what's missing)
Goal: bit-exact parity is gated by EXACT tables/window/constants. The chunk-level model hits
err ≤0.62 dB (dual) / ≤0.19 dB (al_*) — to go sample-exact we need precise values from the binary.
### Static decomp assets IN REPO (use these, don't re-decompile):
- `ghidra-proj/soothe2.rep` — full Ghidra project (vst3 at ImageBase 0x180000000).
- `decomp_funs.txt` (312K lines, ~1640 functions), `fun_map.txt` (2285 addr→FUN), `consts.txt` (11602),
`decomp_dsp.txt` / `decomp_vtables.txt` / `decomp_candidates.txt` / `focus_decomp.txt`. Generators:
`Dump*.java`, `ImportRtti*.java`, `ListFuns.java`, `SearchRefs.java` (+ headless logs).
- RTTI: `rtti_dsp.json` / `rtti_full.json` — class hierarchy
`SpectralProcessor<float,7,1>`, `Soothe2ModuleBase<float,1>`, `FilterGraph<float,6,0x400>`,
`DigitalFilter<float,0xBA,1>`, `IIRFilterExtended<float,1>`, `AudioProcessingModule<float,1>`.
- `handoff/nls_dasm/` — 120 hand-picked `.dis` (twin, iface_18052da00/dbc0, fft, generator, ctor).
- `dsp/` — working C++ transcription (twin, detect, freqpath, spectral, fft_stage) + harness;
`build/twin_check` passes float-parity gate (§2).
### KEY DSP ADDRESSES — decoded / not decoded:
DECODED (formula-level, notes at NOTES_TWIN.md / NOTES_LEVEL.md):
- twin kernel `FUN_180535880`; generator `FUN_180533ec0`; caller `FUN_180536300`; dead sibling `180536f90`.
- level-weight formula `FUN_180530d30` (0x540880/884, warp 0x5406a8=0.87·x/(1+x/7.942), w=0.1^(...)).
- mask-apply entry `FUN_180529fe0` (accumulator 0x5407c8 += w·res; mask *= warp; FIR *= 0x540658).
- FFT-conv loop 0x52b550-0x52b8b5 (plan 0x540530, windows 0x540548/550/598, freq-axis 0x540698=offline const).
- level-path map: `0x563440` (LUT curve +0x188, 6 band-slots, combine→+0x2198), `0x56e3e0` (twin-mask factory),
`0x563ce0` (IIR level-tracker INIT only).
NOT DECODED / MISSING FROM decomp_funs.txt (critical): ⚠️ → РЕШЕНО (2026-08-19/20), см. выше
- `FUN_180529fe0` body — РЕШЕНО (decomp в `/tmp/consumers_out.txt`, mask-цепь в `dsp/framed_model.cpp`).
- `FUN_180563440` / `FUN_180563a60` (LUT curve + gamma + combine), `FUN_180563ce0` — РЕШЕНО,
дизассемблы скопированы в `handoff/nls_dasm/` (`f_563440.dis`, `f_563a60.dis`, `f_563ce0.dis`, `f529fe0.dis`).
- Window `0x540658` — РЕШЕНО (live-захват, см. `handoff/NOTES_CAPTURE.md`; `rtwin_freq_44100.npy`).
- sens source: XML 12.0 → runtime sens_dB≈24.65 (host ×2) not found in dump; `IAT\*0x181bab370` outside dump.
### Bridge model STATUS (2026-08-19):
- `framed_render.py` full STFT frame-render (N=2048, hop=512, sqrt-Hann, twin env tatt=11ms/trel=80ms):
`C(f)=G·LUT(xv)+W·warp(f)^A`, G=1.0850, W=0.2819, A=1.1377 (joint dual+al_* refit).
- Validation: dual q0.1/1/10 @500+2000 err ≤0.62 dB, envRmse@steady ≤0.78 dB; al_* lv3..24 err ≤0.19 dB.
- LUT = slanted al_*-leg (0.366@xv=-0.50 → 0.636@xv=+0.55), replaces flat B.12 (~0.5). Bugfix: clip range
must be [LY.min(), LY.max()] not [LY[0], LY[-1]].
- Remaining structural residual: 0.6 dB systematic on dual 500Hz (q0.1/q1). Live evidence (avg per-bin
gain H=|Y|/|X|): reference mask is FLAT ~-10.2dB across 100-540Hz regardless of Q — model produces
res-shaped notch. Hypo tested: freq-smoothing of C(f) fails (kills 2000Hz). NEXT: scalar per-frame xv
(broadband level, not per-bin am/res) — see /tmp/smoothtest.py (+ edit scalar=True).
## 1. Objective (unchanged since session 1)
Transcribe the decoded soothe2 detector ("twins" 0x180535880/0x180536f90, 2nd-order resonator) into
C++ and reach **bit-exact render parity** with the Reaper reference wavs in `/home/m/soothe-bt/*.wav`
(checking dB/magnitude parity first, then byte-diff).
Path A (bit-exact) chosen. Phase 0,1,2 done. Phase 3,4 advanced. Phase 5 (render diff) is next.
## 2. DECOMPILATION FINDINGS (2026-08-19 — FUN_180563440, FUN_180563ce0 decoded)
### FUN_180563440 — LUT curve evaluation + band combine (222 lines disasm)
Structure: **3 phases per frame**:
1. **1024-bin LUT loop** (0x400 iterations): for each bin k:
- x = clamp(k * 0.0009775, 0, 1.0) = k/1024
- Band config at +0x188: {A(+0x00), B(+0x04), threshold(+0x0c), flag(+0x10), callback(+0x50)}
- **Path 1** (callback exists): vtable call → dynamic LUT
- **Path 2** (flag=1, threshold≠1.0): **power-law** → centered = 2*x - 1, then `sign(x) * 10^(log10(|x|) / threshold)` — this is a **compression curve** controlled by sharpness/threshold
- **Path 3** (default): **linear interpolation**`(B - A) * x + A`
- Output: double-precision at +0x198, stride 8
2. **Twin-mask factory** (FUN_18056e3e0): 6 bands × 1024 bins, stride 0x2000
3. **Combine loop**: stereo (max 2 channels), 6 bands, `1 - sum(band_masks)`
### FUN_180563ce0 — IIR level-tracker INIT (163 lines disasm)
- **341 bins** (0x155 iterations), **order-3 IIR** (3 coefficients per bin)
- Coefficient: **0.1** (`0x3dcccccd` = IEEE 754 float 0.1)
- Initial state: [1.0, 0, 0, 0] and [-1.0, 0, 0, 0] (identity + zero)
- Buffer layout: 3 × (16 bytes coeff) per bin, stored at rcx+0x28/+0x40/+0x58
- **Not the update loop** — init only; UPDATE is elsewhere
### FUN_180529fe0 — Coefficient setup (2051 instructions, in decomp_funs.txt)
- **vtable method** on Soothe2Module<M,1>
- **Lock** at +0x2404dc (atomic test-and-set)
- **PRNG state** at +0x2404e0, LCG with offset 0x3cdca
- **6-iteration coefficient generation** from 0x5408b0 buffer (LCG-indexed)
- **Depth scaling**: `powf(normalized, depth)` at +0x2c
- **Mask assembly**: normalize by 0x1a0 (NFFT), × 0x540870 (level weight), × 0x54088c (sharpness), invert
- **Copy output** via SIMD memcpy (thunk 0x181ba94b0)
### FUN_18052e9b0 — SpectralProcessor main (3167 instructions, in decomp_funs.txt)
- **Same PRNG + coefficient setup** as FUN_180529fe0
- **Band chain**: FUN_18052f500 (interleave) → FUN_18052ee70 (per-bin gain) → FUN_18052d650 (setup) → FUN_18052d920 (window)
- **Buffer alloc**: FUN_18052e190 for 0x540668, 0x540698, 0x5406a8, 0x5406b8-e8 (6 bands)
- **Depth scaling**: `powf(normalized, depth)` with depth at +0x2c
- **Final mask**: `1 - C` (inversion)
- **Window application**: `FIR *= 0x540658` (the live-captured window table)
### Constants verified from binary:
| Constant | Address | Value | Meaning |
|---|---|---|---|
| SCALE | 0x24c3c54 | 0.000977517 | 1/1024 (bin→x) |
| ONE | 0x24c3ea4 | 1.0 | clamping max |
| TWO | 0x24c41e0 | 2.0 | centering (2*x-1) |
| NEG1 | 0x24c4680 | -1.0 | sign flip |
| HALF | 0x24c3d8c | 0.5 | threshold |
| DEPTH_SCALE | 0x24c4334 | 4.0 | depth range |
| DB_CONV | 0x24c43e0 | 8.6859 | 20/ln(10) |
| FLOOR | 0x24c4704 | -6.9078 | ln(0.001) |
| IIR_COEFF | embedded | 0.1 | attack/release per bin |
| IIR_ORDER | embedded | 3.0 | IIR filter order |
| LCG_OFFSET | embedded | 0x3cdca | PRNG state advance |
### Live-captured tables (re-verified):
- `rwin_A0.npy` (0x1930100): 0.5→0.8, **frequency window** (NOT warp formula)
- `rwin_B0.npy` (0x1938180): 0→3.899, **power-law depth curve** (exponent ~0.66)
- `rwin_C0.npy` (0x19401c0): 0.596→0.126, **level-dependent weight**
- `r_freqaxis.npy`: 0→23988.3 Hz, 11.71 Hz spacing (48000/4096)
### Key insight: The warp formula is NOT a table — it's computed at runtime
The empirical `0.87*7.942*x/(7.942+x)` is an approximation of a runtime computation
in FUN_180563440 that evaluates the LUT curve parametrically. The actual LUT has TWO modes:
- **Linear** (default): simple interpolation between A and B
- **Power-law** (flag=1): `sign(x) * 10^(log10(|x|) / C)` — compression curve
## 3. PROOF OF STATE — run this first (5 min)
Everything below must reproduce. If `twin_check` fails, the transcription moved stale.
```bash
cd /home/m/re-tools/dsp && cmake --build build && ./build/twin_check
# expect:
# max rel err = 1.268e-05, PASS = yes (float-parity)
# max sweep rel err = 1.233e-05, SWEEP PASS = yes, exit 0
```
`build/soothe2_dsp.so` and `build/twin_check` are current. CMakeLists already has `twin.cpp` + `twin_check` executable.
## 3. TRANSCRIPTION STATE (summary — details in NOTES_TWIN.md)
- `dsp/twin.hpp` + `dsp/twin.cpp` = twin kernel FUN_180535880:
- build_twin_coeff (FUN_180533ec0 double pipeline → cvtpd2ps floats)
- generator formula (confirmed): `fVar1=sqrtf(param_5)`; `w0=max(2.0,freq)·2π/fs_total`;
`p=(sin(w0)·0.5)/Q`; `A=[1+dVar5, dVar2, 1dVar5]`, `B=[1+param_4, dVar2, 1param_4]`,
`dVar2=cos(w0)·−2.0` (DAT_1824c46b0=2.0).
- cplx_div_exact (rcpps+Newton: `ref=r0·(2den·r0)`, NaN-guard cmpeqps+movmskps, numerator B·conj(A))
- cplx_mul_exact (vfmaddsub213ps form `re=fma(br,ar,bi·ai)`, `im=fma(br,ai,bi·ar)`)
- twin_apply: conj(z1) mirroring 0x1800018b0, z2=z1·z1, out=2·B/A
- **OPEN phase question (byte-exactness)**: conjugate shaping — (-α+π/2) vs (-α) axis. Magnitudes match
either way; final proof needed via phase of a single-bin run before claiming byte-exactness.
## 4. GRID + CALLER (Phase 3 — details in NOTES_TWIN.md §Phase 3)
- Caller FUN_180536300 (decomp 6218, `caller_536300.dis`), scratch ≥52·N complex bytes:
+0..8N A-accum, +8N..16N B-accum, +16N..24N z-powers, +24N..32N rotor-in (filled 1.0),
+32N..36N phase ramp lVar1, +36N..44N rotor-out z1, +44N..52N lVar3(1.0)/lVar4(0.0)
- `0x18052da00(lVar1, scale=2π/(os·sr), N)` = ramp fill; `0x18052dbc0(param_2, lVar4, N)` cplx-interleave
→ out slots get (cos,sin) = e^{+iθ}.
- **Grid law**: `os=*(int*)(cfg+0x240080)=4` (mov DWORD PTR [rbx+0x240080],0x4 @0x18052ce3e),
`sr=*(float*)(cfg+0x24)=44100.0` → generator `fs_total=os·sr=176400`; per-cell phase `2π/176400`;
cell k ↔ freq k/os Hz (0.25 Hz steps). Phase-1 FS=44100 was a valid normalization (res depends on f/fc).
- twin always = FUN_180535880 (type-info compare equal); sibling FUN_180536f90 dead.
- case8: numBands `+0x814`=1, `+0x103`=1; `param_5=10^(sens_dB/20)`, `gain=sqrtf(param_5)=10^(sens_dB/40)`
→ fit gain=4.132 means stored `sens_dB@+0x101 ≈ 24.65`; freq=[band+0x804]=1000.0, Q=[band+0x80c].
## 5. LEVEL-WEIGHT FORMULA (Phase 4 — DECODED, NEGATIVE RESULT — NOTES_LEVEL.md §UPDATE 2026-08-18)
Exact formula from decomp 22330 (FUN_180530d30):
- `base=(2000/(sr·0.5))·iVar5/(bin+1)`, iVar5=NFFT/2+1
- `w8=powf(base,0.25)`
- `v=[0x540880]·0.25·w8·fVar12` (fVar12=4.0 if offline flag [0x5408b8], else 1.0)
- `q=1/(1+v/([0x540880]·4))` (2nd pair 0x540884: q2=1/(1+v2/[0x540884]))
- `dVar1=(sr/[0x1a0])·[0x1ac]·0.001`
- `w=0.1^(1/(max(q·v,floor)·dVar1))`; bufs: 0x5406b8=w, 0x5406c8=1w (and 0x5406d8/6e8 w/ 0x540884)
- consts: c3d3c=0.25, c4334=4.0, c3d8c=0.5, c45b4=2000.0, c3f70=0.1 f32, c3e30=0.001 f64, c3f70=0.1
- **Numerically**: w(500)=w(1000)=w(2000)≈0 (1e-20..1e-0.07), complements≈1 → but the weights are STILL
APPLIED (as 1w ≈ 1) by FUN_180529fe0 (`0x5407c8[band] += 0x5406c8·res_upper + 0x5406e8·res_lower`,
thunk 003c40, kernel 0x1800752e0 = vfmadd213pd) — the form is weight·resonance, not dead code.
- **TILT analysis (2026-08-18, checkpoint NEGATIVE result)**: warp `0x5406a8 = 0.87·x/(1+x/K)`,
`K=exp(2.0723267)≈7.942`, `x=f/2000`, applied by FUN_180529fe0 as `mask *= 0x5406a8` (float, thunk 008700)
BEFORE the FFT-conv. warp: 500→0.211, 1000→0.409, 2000→0.773 (lin ratio 2000/500=3.67).
**BUT warp ≠ empirical tilt (ratio 1.27)**: `C(dB2000)=depth·warp(2000)·LUT_max = 0.864·0.772·0.667=0.445`,
yet red2000=15.2 dB needs `C=0.826` ⇒ warp alone CANNOT produce deep-2000 (dual_b1q). Empirical tilt is the
envelope of the BLEND term (0x5407c8 += weights·res + freq-axis 0x540698) + FFT-conv shaping, NOT warp —
simple tilt→warp substitution in model_lut.py is INVALID. Nothing else decoded yet closes dual_b1q.
- **FUN_180563ce0 decoded (2026-08-18)**: INIT of per-bin IIR level-trackers (0x156=342 bins, order=3.0f
0x40400000, coeff A/B from 0x24c4780/4790, strided fill; third pass +0xffd0 fills 0x40e00000=7.0f).
Not the update loop. Full disasm: `/tmp/opencode/f_563ce0.dis`.
- **Level-path map (2026-08-18, NOTES_LEVEL.md)**: 0x563440 (LUT curve +0x188, gamma, 6 band-slots, combine
→+0x2198, LUT pairs +0x98 at 0x3ff pts) + 0x56e3e0 twin-mask factory (N=1024, DOUBLE, LUT-ramp input) +
0x563ce0 IIR init. 0x563440 NOT in decomp_funs.txt (search via /tmp/opencode/f_563440.dis + level_notes.md).
- **DIAGNOSTIC BRIDGE DONE (2026-08-18) — model_fir.py**: реальная цепочка из FUN_180529fe0
воспроизводит 36 точек до rmse=0.236 dB БЕЗ эмпирического tilt:
`C(f) = g·LUT(log10(L0/res(f))) + w·warp(f)^a`, g=1.221, w=0.358, a=3.143.
- dual500-константа = res_band(500;fc=500)=0.117 Q-НЕЗАВИСИМ, не tilt;
- dual2000-глубина = АДДИТИВНЫЙ терм `w·warp^3.14` (на 1000 Гц вклад ≤0.021 — иначе t1k
рушится; на 2000 = 0.149) — это аккумулятор `0x5407c8 += weights·res + mask` (шаг 5),
НЕ мультипликация warp·LUT (та проваливается >10 dB);
- warp^~3.14 ≈ π — подозрение на кратный каскад (0x540698 freq-axis, ∏0x540688, двойной FFT);
- остаток 0.7 dB при Q=0.1 = форма LUT-колена 0.574, не закрывается per-bin → нужен FFT-уровень.
- **FFT-CONV FULLY MAPPED (2026-08-18, NOTES_LEVEL.md §2026-08-18c, /tmp/opencode/f_52b570.dis)**:
0x535a70 = dispatcher to base-CRT FFT (0x140a10/0x140a70); 0x540548/0x540550/0x540598 = FFT-plan
tables (plan 0x540530, built by 0x52dc30), NOT windows. Per-band loop 0x52b550-0x52b8b5:
mask→fwdFFT→invFFT→fill low/high (xmm13/xmm9 consts)→inv→cplx-op→fwd→`FIR *= 0x540658`→fill→inv→
`FIR[0]=1,FIR[1]=0``*=0x540888`→copy out.
**freq-axis 0x540698**: OFFLINE = constant scalar (NOT exp-formula — exp/rand = online dither).
**Window 0x540658**: only ONE direct ref in dump (read at 0x52b771), written only indirectly —
STATICALLY INVISIBLE → the only plausible source of warp^3.14/LUT-knee. Need runtime capture or
full-pipeline empirical closure.
## 6. PHASE 5 PLAN (updated 2026-08-18 — bridge done, tilt replaced, FFT mined out)
Decision (user): bridge FIRST. **DONE — bridge result above in §5.**
Status: step 2 DONE, step 3 PARTIAL (static analysis exhausted).
1. (15 min) Proof-of-state above; confirm twin + mdl files complete; repo at `60bf3a2`.
2. **Replace empirical tilt** in model_lut.py with `g·LUT(xv) + w·warp(f)^a`**DONE**, rmse 0.236 dB
reproduced, committed `c8f97e4`. Optional refinement fit (2 LUT-knots) reaches 0.167 but is overfit.
3. **Close the 0.7-dB gap**: FFT-conv loop mapped + freq-axis cleared (offline const) — **DONE/PARTIAL**,
committed `60bf3a2`. Remaining unknown = 0x540658 window content (static-invisible). Options:
(a) runtime capture of 0x540658, (b) skip and close empirically in full-pipeline step 5.
4. Runtime values: per-bin IIR level trackers (0x563ce0), 0x540880/884, `[0x1a0]/[0x1ac]`, cell count N.
5. Full pipeline + render diff vs `/home/m/soothe-bt/*.wav`: dB parity → byte parity.
NEXT SESSION START (recommendation): skip deeper FFT mining; go straight to step 5 full pipeline
(mask-chain already in model_lut.py B.12 at 0.236; freq-axis const; FFT-conv reduces to a per-bin
smoothing + 0x540658 shaping that can be fit/absorbed). Runtime capture of 0x540658 only if step 5
stalls on the Q=0.1 knee.
### STEP 5 STARTED (2026-08-18) — dB-parity harness DONE (commit f96bb42)
- `/home/m/re-tools/render_parity.py`: measures steady-state per-tone reduction DIRECTLY on the
reference wavs (Goertzel tone-amp over late steady window; handles 16/24-bit, mono/stereo).
NOTE: Goertzel must use the SEQUENTIAL update form (s2=s1;s1=s0;s0=v+cw*s1-s2) — the tuple-unpack
form is numerically unstable (overflows ~1500 steps).
- Validation: measured dual Q-scan == stored dataset (dual.wav is 4s mono 16-bit; renders 6s stereo 24-bit).
- **dB-PARITY RESULT (canonical B.12, 36 pts, real wavs): TOTAL rmse=0.268 dB**
(dual500 0.130, dual2000 0.387, t1kq 0.320, t1k 0.076).
residual structure: dual2000 -0.80@Q0.1..+0.36@Q10 (LUT-knee), t1kq +0.36..+0.26 (level region),
t1k ±0.1 (good). Refit (g,w,a)=(1.224,0.379,3.405)→0.264; +2 LUT-knots→0.203 (plateau).
- CONCLUSION: B.12 form plateau ~0.20-0.27 dB vs real renders; residual is structural (0x540658
FFT shaping + level-curve region), matches plan expectation. Re-measured-on-wav numbers differ
slightly from the stored model_lut.py dataset (t1kq ~+0.3 dB) — different steady-window method.
- Open: full PIPELINE frame-render (STFT + per-frame mask + time constants) is the remaining big
lift for byte-parity; dB-parity milestone achieved at 0.268 dB.
Candidates that may consume Phase 5 time: source of sens≈24.65 (host ×2 over XML 12.0 — not found in dump),
IAT\*0x181bab370 outside dump (non-blocking), final z¹ phase proof, the 0x563440 LUT curve (+0x188) values.
## 7. FILES LAYOUT
- `/home/m/re-tools/dsp/` — twin.{hpp,cpp}, twin_check.cpp, CMakeLists.txt, build/
- `/home/m/re-tools/model_lut.py` — model **B.12** (Q=xmlq, gain=10^(sens/20), depth 0.8639736175537109,
`C=g·LUT(xv)+w·warp^a`, g=1.221/w=0.358/a=3.143) — 0.236 dB bridge model; PCHIP LUT nodes frozen
- `/home/m/re-tools/model_fir.py` — bridge canonical source (rmse 0.236, committed b1066f3)
- `/home/m/re-tools/framed_render.py` — full frame-render pilot (2026-08-19 params; modes dual|al)
- `/home/m/re-tools/rwin_A0/A1/B0/C0.npy`, `r_freqaxis.npy`, `rwin_warp.npy` — live-window tables (48k)
- `/home/m/re-tools/rtsnap.py` — live-process page snapshotter; snapshots /tmp/rt{A,B,C,D}.{raw,idx}
- `/home/m/re-tools/handoff/decode_rpp_full.py`, `handoff/rpp_allparams.py` — RPP b64-XML full decoder
(trim to len%4==0, `<?xml`@92, incl. nested processorStateData)
- `/home/m/re-tools/soothe_mem.bin` — memory dump, offset=VA0x180000000
- `/home/m/re-tools/decomp_funs.txt` — decomp: FUN_180536300 (6218), FUN_180530d30 (22330),
FUN_180533ec0 (123204), FUN_180529fe0 (missing body! vtable-only)
- `/home/m/re-tools/handoff/` — THIS DOC, NOTES_TWIN.md, NOTES_LEVEL.md, phase1/*.py, nls_dasm/*.dis
- `/home/m/soothe-bt/*.rpp` (decoder `/home/m/re-tools/dsp/...`; also `decode_rpp4.py` referenced) + `*.wav` refs
Кратко: `decomp_funs.txt` 312K строк, RTTI `rtti_dsp.json`, bridge `C=g·LUT+w·warp^a` (rmse 0.236, позже заменён VLAW). Для продолжения читать `AGENTS.md``NOTES_LEVEL_INDEX.md`.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,256 @@
# SESSION HANDOFF — soothe2 detector reverse → bit-exact render
Prepared: 2026-08-18 (checkpoint end-of-session: commits c8f97e4 + 60bf3a2 pushed). Start: READ THIS FIRST.
> **2026-08-20 UPDATE**: см. актуальный канон — `AGENTS.md` и `handoff/NOTES_LEVEL.md`.
> Нижеследующее «NOT DECODED»/«missing body» УСТАРЕЛО: `FUN_180529fe0` mono-path,
> `FUN_180563440/563ce0/563a60` — расшифрованы, полные дизассемблы скопированы в
> `handoff/nls_dasm/` (134 файла). Этот файл — исторический чекпоинт.
## 0. DECOMPILATION INVENTORY (2026-08-19 — what's decoded, where, and what's missing)
Goal: bit-exact parity is gated by EXACT tables/window/constants. The chunk-level model hits
err ≤0.62 dB (dual) / ≤0.19 dB (al_*) — to go sample-exact we need precise values from the binary.
### Static decomp assets IN REPO (use these, don't re-decompile):
- `ghidra-proj/soothe2.rep` — full Ghidra project (vst3 at ImageBase 0x180000000).
- `decomp_funs.txt` (312K lines, ~1640 functions), `fun_map.txt` (2285 addr→FUN), `consts.txt` (11602),
`decomp_dsp.txt` / `decomp_vtables.txt` / `decomp_candidates.txt` / `focus_decomp.txt`. Generators:
`Dump*.java`, `ImportRtti*.java`, `ListFuns.java`, `SearchRefs.java` (+ headless logs).
- RTTI: `rtti_dsp.json` / `rtti_full.json` — class hierarchy
`SpectralProcessor<float,7,1>`, `Soothe2ModuleBase<float,1>`, `FilterGraph<float,6,0x400>`,
`DigitalFilter<float,0xBA,1>`, `IIRFilterExtended<float,1>`, `AudioProcessingModule<float,1>`.
- `handoff/nls_dasm/` — 120 hand-picked `.dis` (twin, iface_18052da00/dbc0, fft, generator, ctor).
- `dsp/` — working C++ transcription (twin, detect, freqpath, spectral, fft_stage) + harness;
`build/twin_check` passes float-parity gate (§2).
### KEY DSP ADDRESSES — decoded / not decoded:
DECODED (formula-level, notes at NOTES_TWIN.md / NOTES_LEVEL.md):
- twin kernel `FUN_180535880`; generator `FUN_180533ec0`; caller `FUN_180536300`; dead sibling `180536f90`.
- level-weight formula `FUN_180530d30` (0x540880/884, warp 0x5406a8=0.87·x/(1+x/7.942), w=0.1^(...)).
- mask-apply entry `FUN_180529fe0` (accumulator 0x5407c8 += w·res; mask *= warp; FIR *= 0x540658).
- FFT-conv loop 0x52b550-0x52b8b5 (plan 0x540530, windows 0x540548/550/598, freq-axis 0x540698=offline const).
- level-path map: `0x563440` (LUT curve +0x188, 6 band-slots, combine→+0x2198), `0x56e3e0` (twin-mask factory),
`0x563ce0` (IIR level-tracker INIT only).
NOT DECODED / MISSING FROM decomp_funs.txt (critical): ⚠️ → РЕШЕНО (2026-08-19/20), см. выше
- `FUN_180529fe0` body — РЕШЕНО (decomp в `/tmp/consumers_out.txt`, mask-цепь в `dsp/framed_model.cpp`).
- `FUN_180563440` / `FUN_180563a60` (LUT curve + gamma + combine), `FUN_180563ce0` — РЕШЕНО,
дизассемблы скопированы в `handoff/nls_dasm/` (`f_563440.dis`, `f_563a60.dis`, `f_563ce0.dis`, `f529fe0.dis`).
- Window `0x540658` — РЕШЕНО (live-захват, см. `handoff/NOTES_CAPTURE.md`; `rtwin_freq_44100.npy`).
- sens source: XML 12.0 → runtime sens_dB≈24.65 (host ×2) not found in dump; `IAT\*0x181bab370` outside dump.
### Bridge model STATUS (2026-08-19):
- `framed_render.py` full STFT frame-render (N=2048, hop=512, sqrt-Hann, twin env tatt=11ms/trel=80ms):
`C(f)=G·LUT(xv)+W·warp(f)^A`, G=1.0850, W=0.2819, A=1.1377 (joint dual+al_* refit).
- Validation: dual q0.1/1/10 @500+2000 err ≤0.62 dB, envRmse@steady ≤0.78 dB; al_* lv3..24 err ≤0.19 dB.
- LUT = slanted al_*-leg (0.366@xv=-0.50 → 0.636@xv=+0.55), replaces flat B.12 (~0.5). Bugfix: clip range
must be [LY.min(), LY.max()] not [LY[0], LY[-1]].
- Remaining structural residual: 0.6 dB systematic on dual 500Hz (q0.1/q1). Live evidence (avg per-bin
gain H=|Y|/|X|): reference mask is FLAT ~-10.2dB across 100-540Hz regardless of Q — model produces
res-shaped notch. Hypo tested: freq-smoothing of C(f) fails (kills 2000Hz). NEXT: scalar per-frame xv
(broadband level, not per-bin am/res) — see /tmp/smoothtest.py (+ edit scalar=True).
## 1. Objective (unchanged since session 1)
Transcribe the decoded soothe2 detector ("twins" 0x180535880/0x180536f90, 2nd-order resonator) into
C++ and reach **bit-exact render parity** with the Reaper reference wavs in `/home/m/soothe-bt/*.wav`
(checking dB/magnitude parity first, then byte-diff).
Path A (bit-exact) chosen. Phase 0,1,2 done. Phase 3,4 advanced. Phase 5 (render diff) is next.
## 2. DECOMPILATION FINDINGS (2026-08-19 — FUN_180563440, FUN_180563ce0 decoded)
### FUN_180563440 — LUT curve evaluation + band combine (222 lines disasm)
Structure: **3 phases per frame**:
1. **1024-bin LUT loop** (0x400 iterations): for each bin k:
- x = clamp(k * 0.0009775, 0, 1.0) = k/1024
- Band config at +0x188: {A(+0x00), B(+0x04), threshold(+0x0c), flag(+0x10), callback(+0x50)}
- **Path 1** (callback exists): vtable call → dynamic LUT
- **Path 2** (flag=1, threshold≠1.0): **power-law** → centered = 2*x - 1, then `sign(x) * 10^(log10(|x|) / threshold)` — this is a **compression curve** controlled by sharpness/threshold
- **Path 3** (default): **linear interpolation**`(B - A) * x + A`
- Output: double-precision at +0x198, stride 8
2. **Twin-mask factory** (FUN_18056e3e0): 6 bands × 1024 bins, stride 0x2000
3. **Combine loop**: stereo (max 2 channels), 6 bands, `1 - sum(band_masks)`
### FUN_180563ce0 — IIR level-tracker INIT (163 lines disasm)
- **341 bins** (0x155 iterations), **order-3 IIR** (3 coefficients per bin)
- Coefficient: **0.1** (`0x3dcccccd` = IEEE 754 float 0.1)
- Initial state: [1.0, 0, 0, 0] and [-1.0, 0, 0, 0] (identity + zero)
- Buffer layout: 3 × (16 bytes coeff) per bin, stored at rcx+0x28/+0x40/+0x58
- **Not the update loop** — init only; UPDATE is elsewhere
### FUN_180529fe0 — Coefficient setup (2051 instructions, in decomp_funs.txt)
- **vtable method** on Soothe2Module<M,1>
- **Lock** at +0x2404dc (atomic test-and-set)
- **PRNG state** at +0x2404e0, LCG with offset 0x3cdca
- **6-iteration coefficient generation** from 0x5408b0 buffer (LCG-indexed)
- **Depth scaling**: `powf(normalized, depth)` at +0x2c
- **Mask assembly**: normalize by 0x1a0 (NFFT), × 0x540870 (level weight), × 0x54088c (sharpness), invert
- **Copy output** via SIMD memcpy (thunk 0x181ba94b0)
### FUN_18052e9b0 — SpectralProcessor main (3167 instructions, in decomp_funs.txt)
- **Same PRNG + coefficient setup** as FUN_180529fe0
- **Band chain**: FUN_18052f500 (interleave) → FUN_18052ee70 (per-bin gain) → FUN_18052d650 (setup) → FUN_18052d920 (window)
- **Buffer alloc**: FUN_18052e190 for 0x540668, 0x540698, 0x5406a8, 0x5406b8-e8 (6 bands)
- **Depth scaling**: `powf(normalized, depth)` with depth at +0x2c
- **Final mask**: `1 - C` (inversion)
- **Window application**: `FIR *= 0x540658` (the live-captured window table)
### Constants verified from binary:
| Constant | Address | Value | Meaning |
|---|---|---|---|
| SCALE | 0x24c3c54 | 0.000977517 | 1/1024 (bin→x) |
| ONE | 0x24c3ea4 | 1.0 | clamping max |
| TWO | 0x24c41e0 | 2.0 | centering (2*x-1) |
| NEG1 | 0x24c4680 | -1.0 | sign flip |
| HALF | 0x24c3d8c | 0.5 | threshold |
| DEPTH_SCALE | 0x24c4334 | 4.0 | depth range |
| DB_CONV | 0x24c43e0 | 8.6859 | 20/ln(10) |
| FLOOR | 0x24c4704 | -6.9078 | ln(0.001) |
| IIR_COEFF | embedded | 0.1 | attack/release per bin |
| IIR_ORDER | embedded | 3.0 | IIR filter order |
| LCG_OFFSET | embedded | 0x3cdca | PRNG state advance |
### Live-captured tables (re-verified):
- `rwin_A0.npy` (0x1930100): 0.5→0.8, **frequency window** (NOT warp formula)
- `rwin_B0.npy` (0x1938180): 0→3.899, **power-law depth curve** (exponent ~0.66)
- `rwin_C0.npy` (0x19401c0): 0.596→0.126, **level-dependent weight**
- `r_freqaxis.npy`: 0→23988.3 Hz, 11.71 Hz spacing (48000/4096)
### Key insight: The warp formula is NOT a table — it's computed at runtime
The empirical `0.87*7.942*x/(7.942+x)` is an approximation of a runtime computation
in FUN_180563440 that evaluates the LUT curve parametrically. The actual LUT has TWO modes:
- **Linear** (default): simple interpolation between A and B
- **Power-law** (flag=1): `sign(x) * 10^(log10(|x|) / C)` — compression curve
## 3. PROOF OF STATE — run this first (5 min)
Everything below must reproduce. If `twin_check` fails, the transcription moved stale.
```bash
cd /home/m/re-tools/dsp && cmake --build build && ./build/twin_check
# expect:
# max rel err = 1.268e-05, PASS = yes (float-parity)
# max sweep rel err = 1.233e-05, SWEEP PASS = yes, exit 0
```
`build/soothe2_dsp.so` and `build/twin_check` are current. CMakeLists already has `twin.cpp` + `twin_check` executable.
## 3. TRANSCRIPTION STATE (summary — details in NOTES_TWIN.md)
- `dsp/twin.hpp` + `dsp/twin.cpp` = twin kernel FUN_180535880:
- build_twin_coeff (FUN_180533ec0 double pipeline → cvtpd2ps floats)
- generator formula (confirmed): `fVar1=sqrtf(param_5)`; `w0=max(2.0,freq)·2π/fs_total`;
`p=(sin(w0)·0.5)/Q`; `A=[1+dVar5, dVar2, 1dVar5]`, `B=[1+param_4, dVar2, 1param_4]`,
`dVar2=cos(w0)·−2.0` (DAT_1824c46b0=2.0).
- cplx_div_exact (rcpps+Newton: `ref=r0·(2den·r0)`, NaN-guard cmpeqps+movmskps, numerator B·conj(A))
- cplx_mul_exact (vfmaddsub213ps form `re=fma(br,ar,bi·ai)`, `im=fma(br,ai,bi·ar)`)
- twin_apply: conj(z1) mirroring 0x1800018b0, z2=z1·z1, out=2·B/A
- **OPEN phase question (byte-exactness)**: conjugate shaping — (-α+π/2) vs (-α) axis. Magnitudes match
either way; final proof needed via phase of a single-bin run before claiming byte-exactness.
## 4. GRID + CALLER (Phase 3 — details in NOTES_TWIN.md §Phase 3)
- Caller FUN_180536300 (decomp 6218, `caller_536300.dis`), scratch ≥52·N complex bytes:
+0..8N A-accum, +8N..16N B-accum, +16N..24N z-powers, +24N..32N rotor-in (filled 1.0),
+32N..36N phase ramp lVar1, +36N..44N rotor-out z1, +44N..52N lVar3(1.0)/lVar4(0.0)
- `0x18052da00(lVar1, scale=2π/(os·sr), N)` = ramp fill; `0x18052dbc0(param_2, lVar4, N)` cplx-interleave
→ out slots get (cos,sin) = e^{+iθ}.
- **Grid law**: `os=*(int*)(cfg+0x240080)=4` (mov DWORD PTR [rbx+0x240080],0x4 @0x18052ce3e),
`sr=*(float*)(cfg+0x24)=44100.0` → generator `fs_total=os·sr=176400`; per-cell phase `2π/176400`;
cell k ↔ freq k/os Hz (0.25 Hz steps). Phase-1 FS=44100 was a valid normalization (res depends on f/fc).
- twin always = FUN_180535880 (type-info compare equal); sibling FUN_180536f90 dead.
- case8: numBands `+0x814`=1, `+0x103`=1; `param_5=10^(sens_dB/20)`, `gain=sqrtf(param_5)=10^(sens_dB/40)`
→ fit gain=4.132 means stored `sens_dB@+0x101 ≈ 24.65`; freq=[band+0x804]=1000.0, Q=[band+0x80c].
## 5. LEVEL-WEIGHT FORMULA (Phase 4 — DECODED, NEGATIVE RESULT — NOTES_LEVEL.md §UPDATE 2026-08-18)
Exact formula from decomp 22330 (FUN_180530d30):
- `base=(2000/(sr·0.5))·iVar5/(bin+1)`, iVar5=NFFT/2+1
- `w8=powf(base,0.25)`
- `v=[0x540880]·0.25·w8·fVar12` (fVar12=4.0 if offline flag [0x5408b8], else 1.0)
- `q=1/(1+v/([0x540880]·4))` (2nd pair 0x540884: q2=1/(1+v2/[0x540884]))
- `dVar1=(sr/[0x1a0])·[0x1ac]·0.001`
- `w=0.1^(1/(max(q·v,floor)·dVar1))`; bufs: 0x5406b8=w, 0x5406c8=1w (and 0x5406d8/6e8 w/ 0x540884)
- consts: c3d3c=0.25, c4334=4.0, c3d8c=0.5, c45b4=2000.0, c3f70=0.1 f32, c3e30=0.001 f64, c3f70=0.1
- **Numerically**: w(500)=w(1000)=w(2000)≈0 (1e-20..1e-0.07), complements≈1 → but the weights are STILL
APPLIED (as 1w ≈ 1) by FUN_180529fe0 (`0x5407c8[band] += 0x5406c8·res_upper + 0x5406e8·res_lower`,
thunk 003c40, kernel 0x1800752e0 = vfmadd213pd) — the form is weight·resonance, not dead code.
- **TILT analysis (2026-08-18, checkpoint NEGATIVE result)**: warp `0x5406a8 = 0.87·x/(1+x/K)`,
`K=exp(2.0723267)≈7.942`, `x=f/2000`, applied by FUN_180529fe0 as `mask *= 0x5406a8` (float, thunk 008700)
BEFORE the FFT-conv. warp: 500→0.211, 1000→0.409, 2000→0.773 (lin ratio 2000/500=3.67).
**BUT warp ≠ empirical tilt (ratio 1.27)**: `C(dB2000)=depth·warp(2000)·LUT_max = 0.864·0.772·0.667=0.445`,
yet red2000=15.2 dB needs `C=0.826` ⇒ warp alone CANNOT produce deep-2000 (dual_b1q). Empirical tilt is the
envelope of the BLEND term (0x5407c8 += weights·res + freq-axis 0x540698) + FFT-conv shaping, NOT warp —
simple tilt→warp substitution in model_lut.py is INVALID. Nothing else decoded yet closes dual_b1q.
- **FUN_180563ce0 decoded (2026-08-18)**: INIT of per-bin IIR level-trackers (0x156=342 bins, order=3.0f
0x40400000, coeff A/B from 0x24c4780/4790, strided fill; third pass +0xffd0 fills 0x40e00000=7.0f).
Not the update loop. Full disasm: `/tmp/opencode/f_563ce0.dis`.
- **Level-path map (2026-08-18, NOTES_LEVEL.md)**: 0x563440 (LUT curve +0x188, gamma, 6 band-slots, combine
→+0x2198, LUT pairs +0x98 at 0x3ff pts) + 0x56e3e0 twin-mask factory (N=1024, DOUBLE, LUT-ramp input) +
0x563ce0 IIR init. 0x563440 NOT in decomp_funs.txt (search via /tmp/opencode/f_563440.dis + level_notes.md).
- **DIAGNOSTIC BRIDGE DONE (2026-08-18) — model_fir.py**: реальная цепочка из FUN_180529fe0
воспроизводит 36 точек до rmse=0.236 dB БЕЗ эмпирического tilt:
`C(f) = g·LUT(log10(L0/res(f))) + w·warp(f)^a`, g=1.221, w=0.358, a=3.143.
- dual500-константа = res_band(500;fc=500)=0.117 Q-НЕЗАВИСИМ, не tilt;
- dual2000-глубина = АДДИТИВНЫЙ терм `w·warp^3.14` (на 1000 Гц вклад ≤0.021 — иначе t1k
рушится; на 2000 = 0.149) — это аккумулятор `0x5407c8 += weights·res + mask` (шаг 5),
НЕ мультипликация warp·LUT (та проваливается >10 dB);
- warp^~3.14 ≈ π — подозрение на кратный каскад (0x540698 freq-axis, ∏0x540688, двойной FFT);
- остаток 0.7 dB при Q=0.1 = форма LUT-колена 0.574, не закрывается per-bin → нужен FFT-уровень.
- **FFT-CONV FULLY MAPPED (2026-08-18, NOTES_LEVEL.md §2026-08-18c, /tmp/opencode/f_52b570.dis)**:
0x535a70 = dispatcher to base-CRT FFT (0x140a10/0x140a70); 0x540548/0x540550/0x540598 = FFT-plan
tables (plan 0x540530, built by 0x52dc30), NOT windows. Per-band loop 0x52b550-0x52b8b5:
mask→fwdFFT→invFFT→fill low/high (xmm13/xmm9 consts)→inv→cplx-op→fwd→`FIR *= 0x540658`→fill→inv→
`FIR[0]=1,FIR[1]=0``*=0x540888`→copy out.
**freq-axis 0x540698**: OFFLINE = constant scalar (NOT exp-formula — exp/rand = online dither).
**Window 0x540658**: only ONE direct ref in dump (read at 0x52b771), written only indirectly —
STATICALLY INVISIBLE → the only plausible source of warp^3.14/LUT-knee. Need runtime capture or
full-pipeline empirical closure.
## 6. PHASE 5 PLAN (updated 2026-08-18 — bridge done, tilt replaced, FFT mined out)
Decision (user): bridge FIRST. **DONE — bridge result above in §5.**
Status: step 2 DONE, step 3 PARTIAL (static analysis exhausted).
1. (15 min) Proof-of-state above; confirm twin + mdl files complete; repo at `60bf3a2`.
2. **Replace empirical tilt** in model_lut.py with `g·LUT(xv) + w·warp(f)^a`**DONE**, rmse 0.236 dB
reproduced, committed `c8f97e4`. Optional refinement fit (2 LUT-knots) reaches 0.167 but is overfit.
3. **Close the 0.7-dB gap**: FFT-conv loop mapped + freq-axis cleared (offline const) — **DONE/PARTIAL**,
committed `60bf3a2`. Remaining unknown = 0x540658 window content (static-invisible). Options:
(a) runtime capture of 0x540658, (b) skip and close empirically in full-pipeline step 5.
4. Runtime values: per-bin IIR level trackers (0x563ce0), 0x540880/884, `[0x1a0]/[0x1ac]`, cell count N.
5. Full pipeline + render diff vs `/home/m/soothe-bt/*.wav`: dB parity → byte parity.
NEXT SESSION START (recommendation): skip deeper FFT mining; go straight to step 5 full pipeline
(mask-chain already in model_lut.py B.12 at 0.236; freq-axis const; FFT-conv reduces to a per-bin
smoothing + 0x540658 shaping that can be fit/absorbed). Runtime capture of 0x540658 only if step 5
stalls on the Q=0.1 knee.
### STEP 5 STARTED (2026-08-18) — dB-parity harness DONE (commit f96bb42)
- `/home/m/re-tools/render_parity.py`: measures steady-state per-tone reduction DIRECTLY on the
reference wavs (Goertzel tone-amp over late steady window; handles 16/24-bit, mono/stereo).
NOTE: Goertzel must use the SEQUENTIAL update form (s2=s1;s1=s0;s0=v+cw*s1-s2) — the tuple-unpack
form is numerically unstable (overflows ~1500 steps).
- Validation: measured dual Q-scan == stored dataset (dual.wav is 4s mono 16-bit; renders 6s stereo 24-bit).
- **dB-PARITY RESULT (canonical B.12, 36 pts, real wavs): TOTAL rmse=0.268 dB**
(dual500 0.130, dual2000 0.387, t1kq 0.320, t1k 0.076).
residual structure: dual2000 -0.80@Q0.1..+0.36@Q10 (LUT-knee), t1kq +0.36..+0.26 (level region),
t1k ±0.1 (good). Refit (g,w,a)=(1.224,0.379,3.405)→0.264; +2 LUT-knots→0.203 (plateau).
- CONCLUSION: B.12 form plateau ~0.20-0.27 dB vs real renders; residual is structural (0x540658
FFT shaping + level-curve region), matches plan expectation. Re-measured-on-wav numbers differ
slightly from the stored model_lut.py dataset (t1kq ~+0.3 dB) — different steady-window method.
- Open: full PIPELINE frame-render (STFT + per-frame mask + time constants) is the remaining big
lift for byte-parity; dB-parity milestone achieved at 0.268 dB.
Candidates that may consume Phase 5 time: source of sens≈24.65 (host ×2 over XML 12.0 — not found in dump),
IAT\*0x181bab370 outside dump (non-blocking), final z¹ phase proof, the 0x563440 LUT curve (+0x188) values.
## 7. FILES LAYOUT
- `/home/m/re-tools/dsp/` — twin.{hpp,cpp}, twin_check.cpp, CMakeLists.txt, build/
- `/home/m/re-tools/model_lut.py` — model **B.12** (Q=xmlq, gain=10^(sens/20), depth 0.8639736175537109,
`C=g·LUT(xv)+w·warp^a`, g=1.221/w=0.358/a=3.143) — 0.236 dB bridge model; PCHIP LUT nodes frozen
- `/home/m/re-tools/model_fir.py` — bridge canonical source (rmse 0.236, committed b1066f3)
- `/home/m/re-tools/framed_render.py` — full frame-render pilot (2026-08-19 params; modes dual|al)
- `/home/m/re-tools/rwin_A0/A1/B0/C0.npy`, `r_freqaxis.npy`, `rwin_warp.npy` — live-window tables (48k)
- `/home/m/re-tools/rtsnap.py` — live-process page snapshotter; snapshots /tmp/rt{A,B,C,D}.{raw,idx}
- `/home/m/re-tools/handoff/decode_rpp_full.py`, `handoff/rpp_allparams.py` — RPP b64-XML full decoder
(trim to len%4==0, `<?xml`@92, incl. nested processorStateData)
- `/home/m/re-tools/soothe_mem.bin` — memory dump, offset=VA0x180000000
- `/home/m/re-tools/decomp_funs.txt` — decomp: FUN_180536300 (6218), FUN_180530d30 (22330),
FUN_180533ec0 (123204), FUN_180529fe0 (missing body! vtable-only)
- `/home/m/re-tools/handoff/` — THIS DOC, NOTES_TWIN.md, NOTES_LEVEL.md, phase1/*.py, nls_dasm/*.dis
- `/home/m/soothe-bt/*.rpp` (decoder `/home/m/re-tools/dsp/...`; also `decode_rpp4.py` referenced) + `*.wav` refs
+150
View File
@@ -0,0 +1,150 @@
## Objective
- Реверс-DSP oeksound soothe2 v1.1.2 VST3 → математическая модель (STFT, детектор резонансов, нотич-синтез). Текущая фаза — поведенческий параметр-свип через рендер Reaper RPP + статический декомпил (без декрипты бинарника пока).
> **⚠️ ИСТОРИЧЕСКИЙ ДОКУМЕНТ (поведенческая модель v4, sim.py, ок. 2026-08-17)**.
> Актуальный канон (bit-exact, P4) — `README.md`, `AGENTS.md`, `handoff/NOTES_LEVEL.md`.
> Здесь — ранняя поведенческая фаза реверса, сохранена как справочник.
## Key Facts
- Пользователь русскоязычный; sudo нет. Объект: `~/.wine/.../soothe2_x64.vst3` (PE32+, base 0x180000000), yabridge 5.1.1, Reaper 7.78.
- Frida невозможна (Seccomp). Дамп памяти — `pread /proc/pid/mem` от родителя. pkill-ловушки: paths без `harneb`/`reaper``pkill -9 -x reaper`, `pkill -9 -f '[y]abridge'`.
- Рендер-проект: `RENDER_RANGE 1 0 0 0 1000`, ITEM обязательно `POSITION 0`. Offline-рендер мгновенный → для живого дампа realtime (`RENDER_1X 1`) + `testtone.wav`.
- VST-state = 92-байт JUCE-заголовок + base64 XML `<SOOTHE2STATE>` + 54 `<PARAM id=... value=.../>`; внутри — UI-state без DSP.
## Breakthrough (исправлено в этой сессии)
- **Формат-адаптивность PARAM**: плагин применяет `<PARAM value>` ТОЛЬКО если строка сериализована в том же формате, что оригинал:
- full-precision (depth, band freq/q...) → `%.16f` (напр. `"0.9000000000000000"`). Короткие строки → схлопывание (fallback).
- short-формат `"X.0"` (selectivity, mix, mode, oversample, resolution) → ЛОМАЕТСЯ от `%.16f`.
- `sweep.py`/`tt_sweep.py` адаптивны: >4 десятичных в оригинале → `%.16f`, 1-4 → то же число знаков, иначе — как есть.
## Синтез модели (поведенческая, v4 — реализована в sim.py, RMSE на settled ≤0.05dB по всем свипам)
```
PARADИГМА (реализована): out(t) = x(t) amount_b(t)·bp_b(t; fc, Q) [субтрактивный нотч, не микш-параллельный]
amount(total) = A(depth)·S(sens)·H(sharp)·M(mode) — LUT из измерений (пересечение, не аналит. фит)
A(depth): 0.5/0.864/1/2/3/5/10/20 → 0.560/0.588/0.598/0.667/0.727/0.820/0.939/0.989
S(sens): 0/6/12/24 → 0.48/0.76/1.0/1.0 (floor при sens=0!)
H(sharp): 1/3/5/10 → 0.20/0.59/0.84/1.0
M(mode): 0 → 0.745 (mode0 слабее), 1.. → 1.0
Q(sel): = 3.0+0.36·sel (2nd-order bandpass; реал. даёт более крутые борта — приближ.)
env(t): one-pole, α_attack=1e^(1/(T_a·fs)), α_release=1e^(1/(T_r·fs))
T_a = 0.02·e^(a/1.955) (0→20мс, 5→258мс, 10→3.3c); T_r LUT из спадов (0→27мс, 5→140мс, 10→15c)
детектор: лауднеss полосы |bp| smoothed 5мс > thr 0.01 → цель amt, иначе 0 (порог между probe 0.0005 и burst 0.2)
выход: x_out = mix/100·… — линейный кроссфейд: (100mix)/100·x + mix/100·fully_reduced
(mix=100 = полная обработка = ref; mix=0 → сухой; проверено на s_corr_mix: монот. линейно)
```
Верификация (settled, RMSE по reduction dB на burst-окне):
| параметр | реал | sim | RMSE | | параметр | реал | sim | RMSE |
|---|---|---|--|---|---|---|---|---|
| дефолт | 7.70 | 7.70 | 0.002 | sharp=1 | 1.11 | 1.11 | 0.002 |
| depth=2 | 9.56 | 9.56 | 0.001 | sharp=3 | 3.72 | 3.71 | 0.006 |
| depth=5 | 14.90 | 14.89 | 0.008 | attack=5 | 5.68 | 5.90 | 0.360 |
| depth=10 | 24.27 | 24.25 | 0.018 | mode=0 | 5.03 | 5.00 | 0.023 |
| depth=20 | 39.43 | 39.40 | 0.054 | mode=2 | 7.70 | 7.70 | 0.002 |
| sens=0 | 2.89 | 2.89 | 0.001 | sens=24 | 7.70 | 7.70 | 0.002 |
Q-профиль (probes-зонд): sim sel1 500→680@540→0.62@700 vs real 8.03→4.54→1.13; 2nd-order не покрывает
крутые борта реала (реал резче ~×3): допустимо как 1-я итерация, RMSE макс по профилю ~1.6dB.
Release-трассы: r0/r1/r2/r5 совпадают (sim против real 1.6s:3.5/4.3, 1.7s:1.5/1.9, 1.9s:0.3/0.3).
## Q/width-зонд (probes500/probes500b, drive 500Hz amp0.2 + справки 0.02)
- Методика: длинный single-FFT на окне 1.7..2.9s (разрешение ~0.7Hz), справки на 500,505,510,520,540,
555,575,600,700 (b-set) / 500,505,515,530,560,640,780 (a-set). Драйв-тон держит детект-клок,
справки зондируют передаточную характеристику в установившемся режиме.
- Данные (a-set) red@freq: sel1: 500:8.0 510:7.9 520:7.7 540:4.5 555:2.7 575:1.6 600:0.7 (плавный хвост)
sel8: 500:8.7 510:9.1 520:6.6 540:1.3 555:0.1 575:0.0 600:0.0 (резкий срез) — Q уверенно растёт с sel.
- Центраئة нотча на 500..510 чуть выше 500 (fs на 505..510) — трекинг слегка субадио.
- Значения на дип-тонах 530/560/640 в b-наборе — из мусора (нет справки) и исключены; a-набор чистый.
- ВАЖНО: dry-файлы дважды перезаписывались провальными dry-рендерами (RENDER_FILE не патчился по
`.wav` напр. burst500L_byp → overwritten probes). ФИКС: патчить RENDER_FILE явно в sed до рендера.
- data: оба svипа предсказуемо монотонны; таблица в summary выше.
## Верифицированные данные (все свипы с корректным форматом)
- **depth (mir)**: монотонно distinct (vs-ref 496k→441k→...→347k@0.85→384k@1.0) — работает.
- **selectivity**: 0/1/2/5 distinct (476k/466k/458k/461k vs ref); 10==20==50==100 (сатурация ≥10).
- **oversample**: 0==ref; 1 distinct; 2==3==4 (сатурация на 2).
- **resolution**: 0 distinct; 1==ref; 2 distinct (все distinct); 3==4.
- **mix**: 0/25/50/75/100 всё distinct монотонно; 100==ref.
- **mode**: 0 distinct; 1==2==3==4==ref.
- **band1 freq**: tt_b1f_678 (дефолт banda freq) == tt_ref бит-в-бит; свип 100..8000 отрендерен.
## Прочие находки
- 60-сек рендер p60_ref/p60_dep0/p60_dep1 подтвердил collapse класса при коротких строках.
- rtdump.py живые дампы (регионы по VA, RA/WA) — DSP-кластер .data меняется между глубинами; раньше не совпали по layout (артефакт noise).
## Work State
### Completed
- Свип-инфраструктура: sweep.py + tt_sweep.py (адаптив формата), рендер, сверочный анализ (spectrum.py медленный, нужно numpy).
- Тест-тон: testtone.wav (2s, multi-tone 110Hz14kHz с AM) для изоляции резонансов; tt_ref==tt_678 (0 diff).
- Базовые свипы по всем основным параметрам.
### Active
- **Извлечена статическая depth-кривая**: LUT 207 float @0x1826170e8..0x182617420, форма `0.302 + 0.698·sin(π/2·x)^0.94` (r²=0.99999), saved `depthcurve.npy`. ИДЕНТИЧНА у ref/dep0/dep1 → вшита в бинарь (build-time), не меняется от параметра = внутренний «depth→amount» маппинг.
- **Адаптивная динамика (burst-тест, band1@500Hz, q≈1, depth 0.864)**: burst500.wav (2s: 0.5s тишины → 1.0s 500Hz burst amp 0.2 → 0.5s тишины, + 1000Hz amp 0.05 фон). `burst500_b1` (fx) vs `burst500_byp` (dry):
- ATTACK: плавное включение нотч-ослабления, τ≈**18 мс** (фит на экспоненту A·(1e^t/τ), stable 17.6/17.7/18.8/18.8), установка −7.7dB бай-в-бай за ~100мс.
- RELEASE: хвост после снятия бурста спадает за τ≈**5 мс** (после 1.5s fx-энергия 24→0 за ~30мс) — быстрый трелинг, симметрия фола не классическая (вероятно фильтр-транзиент, не медленный rel).
- Глубина ослабления НЕ зависит от «порога» — это фикс. нотч на частоте band, amount задаётся curve(depth).
- Рендерится через те же tt-файлы; РЕЗУЛЬТАТЫ в wav — 24-bit (`sampwidth=3`), читать через 24-bit decode (или любой np.frombuffer под sw), не 16-bit!
- **τ-инвариантность** (burst-тест, band1@500Hz, win 10ms, Goertzel, фит к A(1e^t/τ)):
- depth 0→0.86→1: A=6.39→7.70→7.91 dB; τ=18.3→16.8→16.5 ms (τ почти const) → depth контролирует amount, НЕ скорость.
- selectivity: τ РАСТЁТ монотонно 12.3→16.8 ms при sel 0→10 (12.3/12.4/12.6/12.9/13.0/13.4/14.3/16.8) → Q/selectivity = time-const антреккинга.
- depth-amount таблица (settled, band1@500): sel 0..6 монотонно 7.79→−9.08 dB, затем dip sel8=8.79, sel10=7.71 (non-monotonic у высоких sel, нюанс трекера).
- «650Hz notch» в раннем FFT-анализе — АРТЕФАКТ деления на near-zero dry; реальные числа = ratio dry/fx только там, где dry>5% пика (pure 500Hz tone → окно вокруг 500).
- **secret свипа: `patchparam.py`** обязателен для формат-сохранения: state-блок в RPP = base64, wrapped по 128-симв/строка с 2-sp indent и НОВОЙ строкой перед '>'. Ключевое: глубина пишется ТОЛЬКО `%.16f` (короткая `0.0` → collapse!), поэтому CLI `depth=0.0!` форсит полную точность.
- Float-поиск по живому дампу: depth 301 hits (множество копий по bands), selectivity/sharpness → общий бакет (10.015, 9.997), mix → 4 копии 100.0, band freqs (678.76, 8242.67) → 0 точных (хранятся иначе, напр. Hz→binidx или через npf).
- Живой diff dep0 vs dep1 в .data (0x182622130, 0x182622350, 0x1826223c0, 0x18262aca8, 0x182672fe4) — флоаты-состояние меняются; но регион содержит много мусора/nan (это .data буферы рендера, не коэфф).
### Провалы/уроки
- ASLR между отдельными rtdump-запусками сдвигает раскладку регионов → прямого VA-сравнения НЕТ (dep0b 10 регионов, dep1b 9). Только те адреса, что реально в 0x18260a000-0x18267c000 (совпадают) сравнимы.
## Relevant Files
- **`/home/m/re-tools/sim.py`** — поведенческий симулятор (LUT amount + 2nd-order bp Q(sel) + env attack/release + mix/mode). `simulate(x, fc, depth, sens, sharp, sel, mode, attack, release, mix)`. RMSE settled ≤0.05dB (см. таблицу выше).
- **`/home/m/re-tools/verify_sim.py`** — RMSE-сверка симулятора против рендеров (mag@500 трасса). Использование: `verify_sim.py <fx.wav> --params depth=5 attack=5`.
- **`/home/m/re-tools/fit_curves.py`** — фит LUT (остаётся как аналитический эквивалент; LUT в sim.py приоритетнее).
- probes500/probes500b_wav+byp — зонды ширины нотча; prb_*/prb2_* — рендеры.
- `synth_multi.py` (AM-комб 200–3000Hz резонансный), `burst500.wav` + `burst500_b1.rpp/.wav` + `burst500_byp.rpp/.wav` (burst-атак/релиз band1@500Hz), `burst_dep0/dep1/sel0/sel2/sel5.rpp/.wav` (τ-sweep), `comb.wav` + `comb_b1_*.rpp/wav`, `comb_dep_{0..1}`, `depthcurve.npy`. **Рендеры Reaper = 24-bit** — читать с `sampwidth=3`, не 16-bit!
- `/home/m/re-tools/patchparam.py` — патчер PARAM в base64 state с формат-сохранением (128-wrap, `depth=...!` → %.16f).
- `/home/m/re-tools/spec.py`, `probe.py`, `notch.py`, `synth.py`, `mkbase.py`, `tt_sweep.py`.
- `/home/m/soothe-bt/tone1kq.wav` (чистый 1k, -18dBFS), `dual.wav` (500+2000Hz), `resonant.wav` (500Hz-резонанс).
- `/home/m/soothe-bt/t1kq_*.rpp/wav`, `res_only1_*.rpp/wav`, `dual_b1_*.rpp/wav`.
- `/home/m/re-tools/sweep.py` — адаптивный генератор RPP (tpl фиксирован, для смены файла использовать mkbase + tt_sweep).
- `/home/m/soothe-bt/tt_base.rpp` (template: testtone + дефолтные парамы), `tt_ref.wav`.
- `/home/m/soothe-bt/s_corr_{sel,os,res,mix,mode}*.wav` — верифицированные свипы на resonant.wav.
- `/home/m/soothe-bt/render_v5.rpp`, `s_ref2.wav` (бит-в-бит оригинал).
## Фаза 3: Трекинг подтверждён (aug 16) — ARCHITECTURE REVISION
- **Вход-зависимость**: ред@1k растёт с уровнем сигнала (band1=1000, тон 1k):
`-27→-21→-15→-12→-9→-6→-3 dBFS``5.80→7.75→9.91→11.05→12.21→13.40→14.60 dB`. amount растёт с уровнем детектируемого резонанса (не бинарный порог).
- **Трекинг нотча** (fresh renders `trk_b1_*`, вход tone1k 1k, band1∈{500,1000,2000}, длинная FFT): нотч ВСЕГДА на fc=тон=1k, band-частота НЕ двигает позицию режектора:
- band=500 → 11.91dB, band=1000 → 15.82dB, band=2000 → 11.91dB, band1 off → 9.80dB, sens=0 → 9.80dB.
- Единственный вклад band-EQ — ВЕС глубины детекции в области (совпадение band~тон даёт +4dB; off/sens0 2dB).
- **Мульти-пики**: dual (500+2000), оба нотча активны одновременно при sel=1..20 (~10dB оба) → детектор находит НЕСКОЛЬКО резонансов; selectivity НЕ регулирует число нотчей на явных тонах.
- **Пользователь (иерархия)**: детектор оценивает каждую частоту спектра; band-EQ формирует его ВХОД (усиление/ослабление детекции по областям, у band2 sens может быть 12); selectivity = отбор пиков; sharpness = форма нотча; depth = глобальная глубина.
- **Reaper-ловушка**: tt_sweep с ОТНОСИТЕЛЬНЫМ out_wav создаёт ПОДДИРЕКТОРИЮ → всегда абсолютный путь в RENDER_FILE.
- Файлы: `trk_base.rpp`, `trk_b1_{500,1000,2000,eqoff,sens0}.rpp/.wav`, `sel_base.rpp`, `sel_{1,3,10,20}.rpp/.wav`, `lvl_{03..27}db.wav` + `lvl_t_*.rpp/.wav`.
### Фаза 3b: amount зависИТ от уровня сигнала (не просто порог)
- lvl_sweep (тон 1k, band1@1000, depth=0.864): ред@1k = 5.80 / 7.75 / 9.91 / 11.05 / 12.21 / 13.40 / 14.60 для 27..3 dBFS.
- При depth=10: 21.67 / 24.15 / 26.63 / 27.86 / 29.09 / 30.33 / 31.56. Наклон d(ред)/d(level) ≈ 0.370.41 dB/dB обоих depth → amount ∝ уровень^p (монотонно с насыщением), НЕ бинарный порог. depth — масштаб, не floor.
- Обе кривые монотонны и близки по форме (отношение ~2.2 при громком, ~3.7 при тихом → форма зависит от depth слабо).
### Фаза 4 (aug 16): КАРТА EQ-ВЕСОВ ДЕТЕКТОРА
- **`on` полосы = enable детекторного веса**; off-полоса её sens НЕ отдаёт в детекцию. Подтверждения:
- eqT_b1_b2/b3/b4/b2b3b4 и trk_b1_500 (sens off-полос любая) ВСЕ = 11.91dB (только от b1 sens12); eq_off_sensfac (все off, factory sens) = 9.80dB = нейтраль.
- eq1778_b2s_12 (b2 on=0) = 11.81, b1on12_b2s12 (b2 on=1) = 17.60 — ЕДИНСТВЕННАЯ разница band2 on.
- Баг в раннем clear_bands сбрасывал b1 sens→0 (eq_b1_on был sens0=нейтраль 9.80; правильный net_b1on12 sens12 = 11.91).
- **S(sens) при совмещённом весе (band1@1k, тон 1k)**: sens 0/6/9/12/18/24 → boost над полом 0.00/3.16/4.65/6.02/6.02/6.02 dB.
boost = 6.02·min(1, sens/12), насыщение +6dB при sens≥12 (не монотонно из-за сатурации детектора, не бага).
- **W(f) — колокол EQ-веса** (тон 1k, одна полоса on sens12, варьирую band freq):
- band1 (q≈1): 500→11.91(+2.11), 600→12.80(+3.00), 750→14.28(+4.48), 850→15.19(+5.39), 950→15.75(+5.95), 1000→15.82(+6.02), 1050→15.76, 1200→15.05(+5.25), 1500→13.44(+3.64), 2000→11.91(+2.11), 2500→11.16(+1.36). Симметричный широкий колокол (первый октав хвост +2.1dB, не спадает к 0).
- band2 (q≈4.5): 600→10.15(+0.35), 750→10.78(+0.98), 850→11.97(+2.17), 1000→15.80(+6.00), 1200→11.68(+1.88), 1500→10.34(+0.54) — РЕЗКИЙ колокол. **Q полосы управляет шириной детекторного веса** (q=4.5 на −1 октаве теряет ~полностью, q=1 теряет только ~66%).
- **Позиция нотча**: band freq не двигает режектор (всегда на резонансе тона); EQ-веса модулируют ТОЛЬКО глубину.
- Новый модельный блок: `red(f_tone) = floor(level) + Σ_on boost_i`, `boost_i = 6.02·min(1,|sens_i|/12)·H_q(f_tone; fc_i, Q_i)`.
- Файлы: `wf_{600,750,...,2500}.wav` (band1 q-sweep), `ws_{6,9,18,24}.wav` (aligned sens), `b2q_{600..1500}.wav` (band2 q=4.5 sweep), `b1on12_b2{on12,s12}.wav`.
### Фаза 5 (aug 16): STFT-параметры детектора (Stage 1)
- **Хоп/задержка**: tone-jump зонд tj.wav (1k→1.5k в t=2.0s) → нотч пересаживается на новый резонанс за ~1 кадр ≈ 18мс (hop ≤ 512 сэмплов). Трекинг чирпа 400→2000Hz в реальном времени, dt≈0.03→0.05s (шум оценки FFT-бина, значимой задержки нет).
- **Разрешение по частоте**: пары 1000+1040 → сливаются в один широкий нотч; 1000+1100 разделяются (центры ≈996/1104, но провал между ними всё ещё глубокий); 1000+1200 → два чётких отдельных нотча с полным восстановлением на 1100. → окно FFT ≈ 10242048 (бины 2143Hz), multiple-нотч подтверждён.
- **Энерготрекинг (fx@f по окну 2048, sync-Goertzel)**: до прыжка fx@100024dB (ред ~14dB), после — fx@1500 ≈ −23.8dB. Двойной бурст (gap 10..100ms) → нотч на том же тоне не переоткрывается между бурстами (ред остаётся ~4dB floor) — bурст probe (осб) указывает на фиксированный window-центр, не пер-открытие.
- Вывод для v5: STFT с окном ~2048, хоп ~5121024; window function Hann; детектор по |X| per-bin → локальные пики (selectivity); EQ-веса W(f) как множитель sensitivity per-bin; per-пик нотч (bandstop Q=sharpness).
- Файлы: `chirp_log_400_2000.wav`, `chirp_trk.rpp/.wav`, `pair_{1040,1100,1200}.wav` + `pair_trk_{40,100,200}`, `dburst_{10,20,50,100}.wav` + `dburst_trk_*`, `tj.wav` + `tj_trk.rpp/.wav`.
### Фаза 6 (aug 16): КОЛОКОЛ EQ-ВЕСА + SHARPNESS + level-floor (Stage 2 фит)
- **H(f) — колокол**: `H(f) = 1/sqrt(1 + (Qeff·A)²)`, `A = f/fc fc/f`. Qeff = **1.54·q^1.33** (q=1→1.54, 2→3.32, 3→5.64, 6→16.8). Воспроизводит кривую band1 (wf_*) с ошибкой ≤0.15dB по всему диапазону 500..2500.
- **Sharpness → Q нотча**: psh_{1,3,5,10} (probes500, drive 500·0.2, band1@500): центр 505Hz red 1.3/4.3/6.8/8.7dB; ширина Q_notch≈1.15·sharp (sh10→~11, sh5→~6, sh3→~3, sh1→~1.2). Центр-глубина НЕ постоянна — растёт с sharp (1.3→8.7), т.е. sharp масштабирует amount И Q.
- **level-floor (нейтраль)**: `neut_{24..3}` (все полосы off/sens0, тон 1k): red = 2.22/3.36/4.85/5.71/6.65/7.65 dB при 24/18/12/9/6/3 dBFS rms. Монотонно, наклон ~0.31 dB/dB. `al_{*}` (b1@1000 sens12): 5.80/7.75/9.91/11.04/12.21/13.39 → boost = 3.59/4.39/5.06/5.33/5.56/5.74, **насыщается к ~6dB** с уровнем (не аддитивен в dB!).
- Модель amount: `red(f) = curv(level·G_eq(f))` с насыщением (не линейный сдвиг в dB). depth масштабирует (фаза 3b). Для sim: 2D LUT level×W.
- Файлы: `lvl_tone_lv{24,18,12,9,6,3}.wav`, `neut_{*}.rpp/.wav`, `al_{*}.rpp/.wav`, `psh_{1,3,5,10}.rpp/.wav`.