Files
soothe2-re/roadmap.md
T
Matiq 59619e4648 docs: roadmap B.12-B.15 journal + README rewrite (framed_render canonical, honest metric)
- roadmap: current status header (decomp closed, frame-render model, honest metric),
  new assets (fraed_render/render_parity/handoff/nls_dasm/rwin tables), journal
  B.12 (bridge), Phase5 (dB parity 0.268), B.13 (res_power+decomp LUT/IIR),
  B.14 (Q-dep rp + metric artifact correction), B.15 (joint free-knot LUT, dual mean 0.027).
- README: rewrite to framed_render.py as canonical model; 3-line model formula;
  structure + corpus tables updated; honest-metric warning; next steps focused.
2026-08-19 17:09:21 +03:00

550 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Roadmap: bit-exact реверс DSP-ядра oeksound soothe2 (v1.1.2)
## Цель
Воспроизвести DSP-путь soothe2 в виде **собираемого standalone C++** с **побайтово идентичным**
выходом (bit-exact) на верификационных свипах, с пониманием логики и подписанными функциями.
## Результат
`re-tools/dsp/``*.cpp/*.h` (классы DSP, подписаны), `CMakeLists.txt`, `harness.cpp`
(WAV16 → рендер → WAV24), `dsp_notes.md` (карта функция↔смысл, константы, LUT),
`verify_bit_exact.py` (прогон всех свипов, побайтовое сравнение).
## Текущий статус (2026-08-19)
**Статический декомп DSP-ядра — В ОСНОВНОМ ЗАКРЫТ** (A.2/A.3/FFT/twin/generator/level-path/LUT/IIR декодированы,
`.dis` в `handoff/nls_dasm/`). **Численная модель — frame-рендер `framed_render.py`** (STFT N=2048, hop=512,
sqrt-Hann, twin-env tatt=11ms/trel=80ms, LUT+additive-warp+res_power). **Честная тримнутая метрика**
(длина выхода = длина входа): Q-dep rp → **mean=0.175 dB** (q0.1/q10 идеальны, боттлнек q1@2000=0.71);
joint free-knot LUT + dual+al_* фит → **dual mean≈0.027 dB** (B.15). Цель bit-exact (Phase C) — в работе;
остаток структурный (форма LUT в зоне xv<0.3, warp-терм).
```bash
# Простейший запуск текущего канона (Q-dep rp):
cd /home/m/re-tools && python3 framed_render.py dual
# q0.1: 500 +0.00 / 2000 +0.00 · q1: 0.26/0.71 · q10: +0.09/+0.00 · mean 0.175
```
**ВАЖНО про метрику**: ранние записи "mean=0.160/0.144" считались на выходе НЕтримнутой длины
(`nfr*HOP+N`) и оказались артефактом; честный сравнительный базлайн для scalar `rp=0.0169` = **0.280**
(не 0.160). Все новые числа ниже — на ТРИМНУТОЙ метрике.
## Задел (уже есть)
- Ghidra-проект `ghidra-proj/soothe2.rep` + скрипты `Dump*.java`/`ImportRtti*.java`/`Diag.java`.
- RTTI иерархия DSP: `SpectralProcessor<float,7,1>`, `Soothe2ModuleBase<float,1>`,
`Soothe2Module<float,1>`, `FilterGraph<float,6,0x400>`, `FilterGraphGrid<float>`,
`DigitalFilter<float,0xBA,1>`, `IIRFilterExtended<float,1>`, `AudioProcessingModule<float,1>`
(`rtti_dsp.json`, `rtti_full.json`).
- Декомпиляция vtable-слотов DSP-классов (`decomp_dsp.txt`, `decomp_vtables.txt`,
`decomp_candidates.txt`) — 6634 строки C, 127 неразрешённых `FUN_*`, ~30 констант `DAT_1824c*`.
- Поведенческая модель `sim_v5.py` (RMSE ≈ 0.3dB): base_red, W(f)=6.02·min(1,sens/12)·H,
H=1/√(1+(Qeff·A)²), Qeff=1.54·q^1.33, sat(level), Q_notch=qn·(1g)+1, per-frame env, WOLA-маска.
- LUT: depthcurve (207 float @0x1826170e8, `0.302+0.698·sin(π/2·x)^0.94`, r²=0.99999).
- Рендер-свипы в `/home/m/soothe-bt/` (506 wav + 609 rpp) — эталон для bit-exact.
- BFS-замыкание call-graph от сидов: `fun_map.txt` (2035 функций), `decomp_funs.txt` (1640 декомпилировано,
312K), `consts.txt` (11602 констант) — после фикса `getInstructions()` в `DumpFuns.java`.
- **Фрейм-рендер**: `framed_render.py` — полный STFT-рендер (N=2048, hop=512, sqrt-Hann,
twin-env, `C(f)=G·LUT(xv)+W·warp(f)^A`, `gain=(1C)·res^(rp0·Q^drp)`); режимы `dual|al`.
- **dB-parity харнесс**: `render_parity.py` (Goertzel-замер steady-state тона, 16/24-бит, mono/stereo).
- **Критичные дизассембли**: `handoff/nls_dasm/{f_563440,f_563ce0,f529fe0}.dis` (защищены из /tmp).
- **Живые таблицы** (48k): `rwin_{A0,A1,B0,C0}.npy` (частотное окно/power-law/level-weight), `r_freqaxis.npy`.
- **Handoff-документация**: `handoff/SESSION_HANDOFF.md` (инвентарь декомпа, трансляция),
`handoff/NOTES_TWIN.md`, `handoff/NOTES_LEVEL.md` (LUT-нога + res_power протокол).
## Контракт из manual (`soothe2_ManualFAQ.pdf`, v1.0.0)
| # | Факт | Следствие для DSP |
|---|---|---|
| M1 | Soft/Hard: выбор режима меняет всё; depth 3.0 в soft ≠ hard (стр.7). Soft менее level-dependent | mode = глобальный препроцессор детектора (перемасштаб глубины/масштаба) |
| M2 | Depth — референтный dB (−18..18), реальная глубина до 60dB (стр.8) | вход depthcurve-LUT; clip 60dB = `red.clip(0,60)` в sim |
| M3 | Sharpness: выше = глубже и уже (стр.8) | Q_notch растёт с sharp; подтверждает `Q≈qn·(1g)` |
| M4 | Selectivity: выше = только сильные резонансы; 0 = «everything must go» (стр.9) | selectivity = отбор/порог пиков, число нотчей НЕ ограничивает |
| M5 | Attack/Release: референтные константы, **реальные времена частотно-зависимы**, атака быстрее на ВЧ (стр.11) | Env НЕ один one-pole; нужен частотный масштаб времени — замер τ(f) + код |
| M6 | Oversample = «расчёт reduction-фильтра в более высоком спектральном разрешении» (стр.11) | это интерполяция детекторной сетки → фильтра, НЕ up/downsample аудио |
| M7 | Resolution = «частота обновления детекции и фильтра» (стр.11), eco/high/ultra | период пересчёта коэффициентов фильтра |
| M8 | Stereo: link 100% = сумма каналов для анализа + общий фикс; 0% = dual mono; balance глоб+per-band; L/R vs M/S | детектор на sum; коррекция симметрична/раздельна |
| M9 | Trim: «регулирует ТОЛЬКО wet»; Delta = xwet; Mix 0..100% (стр.14) | порядок: detect→notch→trim→mix/delta/bypass |
| M10 | Банды: 2 cut + 4 general; general: peak/shelf/reject/tilt (стр.17-19) | `FilterGraph<float,6,0x400>`, `DigitalFilter` мультитипный |
| M11 | Кривая = «inverse EQ / side-chain EQ», сумма band-кривых = белая кривая (стр.17) | `red(f)=base+ΣW_i·H_i` подтверждён |
| M12 | Sidechain: детект по второму входу; Input trim — скрытый предусилитель анализа (стр.26) | analysis имеет отдельный вход/гейн — граф с 2 входами |
## Топология пайплайна (Приложение A, стр.30 — OCR `pipeline_ocr.txt`)
```
main inputs ──┐
sidechain ────┤ (sidechain enabled?)
input trim ──► analysis (детекция; depth/sharpness/selectivity; EQ curves из per-band balances)
stereo apply + balance (channel link + global balance)
ms encode? ──► processing (нотч-синтез в M/S) ──► ms decode?
mix ──► bypass? ──► output
```
- **mid/side ручка**: обработка в M/S-области (`ms encode?`/`ms decode?`) — стерео-путь подтверждён диаграммой.
- GUI/visualisation (depth/sharpness/selectivity, EQ curves, per-band balances) — побочная ветвь, в DSP-ядро не входит (кроме параметров).
## Фазы
### Фаза 0 — Снятие диаграммы пайплайна (сделано)
0.1. OCR стр.30 (`tesseract`, PNG в `/tmp/opencode/manual/p30_s4.png`) → `pipeline_ocr.txt`. ✅
### Фаза A — Полный статический RE (Ghidra headless)
- A.1. Декомпилировать все 127 `FUN_*` из vtable-слотов 8 DSP-классов → полный `processBlock`-путь.
- A.2. Извлечь float-константы `DAT_1824c*` + все LUT-таблицы в `.data` (поиск массивов float).
- A.3. **Идентифицировать FFT** (twiddle/бит-реверс/радикс) и окно (формула `sin²` vs LUT) — ключ bit-exact.
- A.4. Найти частотно-зависимые attack/release (M5): функции масштабирования времени по частоте.
- A.5. Найти oversample/resolution-путь (M6/M7): интерполяция сетки, период обновления.
- A.6. Сопоставить топологию с диаграммой (0.1) и stereo/link/balance (M8).
### Статус фазы A (~90% — декомп закрыт, остались runtime-значения/окно 0x540658)
- **A.2 константы — готово**: `consts.txt` (11602 из инструкций BFS-замыкания, 2670 в DSP-диапазоне
0x1824c00000x182700000) из повторного прогона `DumpFuns.java`. Точная double/f32-интерпретация —
`consts_double.txt` (чтение PE напрямую, `.data` vs `.rdata` через RVA).
- **A.3 ОКНО — найден**: `FUN_1805356f0` генерирует **Hann** `w(i)=0.5·(1cos(2πi/N))`
(double 2π @0x1824c4e00/4248, 1.0 @4da0/4140, 0.5 @4c90/4050; хвост через `cos`, чётные пары через
SIMD `divpd`+`thunk_FUN_181a114e0(=cos)`). Размер N: `FUN_18052e130`
`N = 2^floor(log2(sr/44100))·N0` (44100 @0x1824c45ec, 2.0 @41e8). **Нормализация окна** в
`FUN_18052df60`: сумма по окну → `scale=1/(mean/N)`, потом `/= (fsize @+0x1ac)`.
- **A.3 FFT — НАЙДЕН (свой)**: Vorbis-планировщик `FUN_181384180` — не тот (это декодер).
Настоящий DSP-FFT: `FUN_180008140` = init FFT-spec (`N=1<<log2`, масштаб `DAT_181c5cd10(=1.0)/N`
для inverse, ветки float/double/complex), строит twiddle/bitrev через `FUN_18003a2a0`
(radix-2/3/5 для малых N), `FUN_18003f1a0`+`FUN_18003f600` и `FUN_18003b6c0` (N≥2^18: бит-реверсовая
пермутация с XOR-маской знака `DAT_181c5e4e0`). Внешних lib (kissfft/fftw/MKL) нет.
Xrefs на init только из диспетчерской таблицы `182615ef0` (+ .pdata) — вызов косвенный.
- **Диспетчерские таблицы (PACE/ModuleDecryptor)**: все DSP-примитивы вызываются через
`thunk_FUN_181ba94b0@0x180001850` → слот из таблиц `PTR_FUN_1826159c8/5a88/6408...` по глобальному
индексу `DAT_1826159a0`. 36 таблиц, 245 уникальных функций-целей (`dispatch_targets.txt`).
Примеры ядер: `FUN_1800025e0` = double-вектор add (развёртка по 16), `FUN_18003fa20` =
комплексное умножение a[i]·b[i] (фрейм-пар, 425б). Для bit-exact нужен порядок double-операций.
- **Константы детектора**: `10^((dB)/20)` через `expf((30·X90)·0.115129255)`,
`0.115129255=1/(20·log10e)`, `6.907755=ln1000`, `0.707`/`0.51`/`0.54`/`0.001`,
маппинг резонанса `(x0.707)·0.5+1` в `FUN_1805316e0`.
### Фаза B — Реконструкция C++ (bit-exact)
- B.1. Классы 1:1: `Soothe2FilterGraph::processBlock`, `FilterGraphGrid`, `DigitalFilter`,
`SpectralProcessor` (STFT), `AudioProcessingModule`, `IIRFilterExtended`, env-модуль с
частотным масштабом (M5), интерполятор oversample (M6), периодичность resolution (M7).
- B.2. Точный порядок FP-операций float32, без перестановок; буферный фрейминг независимо от блока хоста.
- B.3. Имена по смыслу + подписи, сверяемые с моделью sim_v5 (base_red, W, Qeff, sat, Q_notch).
- B.4. Модули по manual: mode (M1), input trim/sidechain (M12), trim/delta/mix/bypass-порядок (M9),
stereo/link/balance + ms encode/decode (M8).
### Фаза C — Сборка и bit-exact-верификация
- C.1. `libsoothe2_dsp.so` + CLI-харнесс (WAV16→рендер→WAV24, как Reaper).
- C.2. Критерий: каждый свип (`trk_*`, `wf_*`, `qmap`, `sens`, `lvl`, `neut`, `al`, `psh`,
`dual`, `pair`, `chirp`, `burst`, `tj`) — **побайтовая идентичность** int24 PCM с рендерами.
- C.3. Итеративный цикл diff → локализация блока → фикс → повтор.
## Риски
- **FFT bit-exact**: DSP-STFT FFT найден (планировщики-гиганты + stage-ядра, см. recap ниже);
воспроизводим напрямую. Главный нерешённый риск bit-exact — порядок double-операций в dispatch-ядрах.
- **Хост-зависимость**: уточнить точный размер буфера/фрейма (рендеры при разных RENDER_RANGE должны давать одинаковые байты).
- **Stereo**: все текущие свипы mono; для бит-exact графа M8 нужны стерео-рендеры (link/balance/ms).
- **Честная метрика**: тримнутая длина выхода обязательна; НЕ-тримнутые замеры дают ложный сдвиг ~0.2 dB
(артефакт B.14). Все fit-цели перепроверять на `render_trim`/`framed_render.synthe`.
## Открытые вопросы
1. **Форма LUT в зоне xv<0.3**: joint-fit свободных узлов даёт dual≤0.07, но al_* lv24 dC=+0.10 —
нужна структурная кривая FUN_180563440 (linear/power-law по флагу полосы) вместо Pchip-эмпирики.
2. **warp-терм**: эмпирический `0.87·7.942·x/(7.942+x)` — аппроксимация runtime-вычисления
(не таблица); для bit-exact заменить на реальный расчёт FUN_180563440.
3. **Стерео-верификация (M8)** — приоритет mono-путь или сразу стерео-граф.
4. **0x540658 window** — статически невидим; живые копии в `rwin_*.npy` (48k) требуют
warp-нормализации под 44.1k рендеры.
## Phase A progress update (recap)
### FFT transform (found)
- **Recursive FFT planners ("giants")**: 7 monolith funcs (~14.4K each) = type variants:
18001f260, 1800246e0, 18002f980, 180029ca0, 180035980, 18003b900, 18004ff80
Recurse on `log2 - factor` with `param_5+1` depth; build stage plan in `param_1+0x78` array.
- **Factor tables** (int, index=log2): `DAT_181c5e0fc` (small=4/8/9/13), `DAT_181c5e15c`,
`DAT_181c5e3fc` (nonzero => use giant path in FUN_18003b6c0).
- **FUN_18003b6c0** (bit-reverse+plan, N big): if `DAT_181c5e3fc[log2]!=0` call giant `FUN_18003b900`,
else small plan via DAT_181c5e4e0 XOR mask permute.
- **Base small FFT**: `FUN_18002e360` (radix2/3), `FUN_180023860` (float variant).
- **Twiddle**: copied from static base `DAT_182616800` into scratch by `FUN_180039b00`.
- **Spec init FUN_180008140** (from dispatch table `182615ef0`): builds plan; dispatch-only callers.
- Giants/planners = integer address gen (no float math) => executors are separate (dispatch kernels).
### Spectral layer located (named C++ classes via RTTI):
- `SpectralProcessor<float,7,1>` vtbl @180529610 (1154) + methods around 180529xxx/18052exxx.
- `Soothe2Module<float,1>`, `Soothe2ModuleBase`, `IIRFilterExtended<float,1>`, ModuleDecryptor.
- range 0x180500000-0x180530000 listed (1051 fns) -> funs_range.txt overwritten.
### Block processor chain (audio->out):
- `FUN_18052e260(param_1, n)` : reads det? calls **FUN_180536300(param_1+0x3d8, in, out, det, n)**
- `FUN_180536300` (475): normalize `DAT_1824c4248/(freq)`, FUN_18052da00/db50/d990/dbc0 (smoothing),
FUN_180535880, FUN_180536f90 (249, detector update via dispatch).
- Param setup fns: FUN_18052e9b0 (3167), FUN_180529fe0 (2051) — float math on params (DAT_18262b700 block-sizes etc.).
### Audio chain (xrefs confirmed)
- Soothe2Module::processBlock (`0x18052b??` call at 18052b99c) -> **FUN_18052e260** (reads, calls block proc)
- -> **FUN_180536300(param_1+0x3d8, in, out, det, n)** (also called from 0x18058e3df)
- which calls: FUN_18052da00(normalize), FUN_18052db50(fill/0), **FUN_180535880**, **FUN_180536f90**,
FUN_18052d990, FUN_18052dbc0 (smoothed combine), FUN_18052db50.
- All small DSP ops = PACE stubs `__std_type_info_compare` + `thunk_FUN_181ba94b0` (dispatch).
- vftable SpectralProcessor = `PTR_FUN_182615990`; methods listed in rtti_dsp.json (many are ModuleDecryptor/PACE slots, NOT dsp).
- Param/curve generators (exp/log/pow/tan based, DAT_1824c4xxx): FUN_1805316e0 (6238), FUN_180529fe0 (2051), FUN_18052e9b0 (3167).
- Dispatch index global `DAT_1826159a0` written only in PACE wrappers 18000101b/180001046.
- big spectral-area functions are mostly param/memory/PACE; the real dsp sticks are dispatch kernels (245 targets).
### DSP kernel map (xref cross-check)
- **Complex multiply** `FUN_18003fa20(double*, cplx[][16], n)` — called ONLY from `FUN_180008440` (init area, twiddle build) — NOT a dispatch slot.
- **Double mul** `FUN_180008500` — dispatch slot @182615f20; called from **FUN_18000c5e0** (double FFT stage kernel, 507B):
loop over 0x40-chunks: `thunk_FUN_1801560a0(param_2,re,im,0x40)` (load twiddle), `FUN_180008500(param_1,re,n)`,
`FUN_180008500(param_1,im,n)`, `FUN_180044700(im,re,param_3,n)`.
- **Double add** `FUN_1800025e0` slot @1826159e8; **float max** `FUN_1800104e0` slot @1826164a0; `180010860` = vmax/vmin with const.
- Decompiler entry-point trick: `getFunctionAt` misses overlapping/obfuscated entries => use `getFunctionContaining` (DumpList updated).
- OBtained: full FFT-plan model, factor tables, per-type kernels, block->detector->mask flow.
- Next sub-goal: trace the FFT-stage kernel call order used by detector (which plan fields drive per-stage twiddle/mul)
=> then C++ re-implementation is a direct transcription of that order.
### Transform runtime (tracing complete -> next: reimpl)
- Plan fields consumed via plan->thunk indirection: `thunk_FUN_1801560a0` -> `PTR_FUN_1826178a0`; `FUN_18014ec20` thunk -> `PTR_FUN_182617828/850/878` (3 twiddle-loaders).
- Stage kernels:
- **FUN_18000bfc0** (complex stage, slot @182616208): loop 0x40: load-twiddle(re,im) ; `FUN_180008440`(cplx-mul, slot @182615f08) twice (re,im) ; `FUN_1800437c0`(accum). out+=0x200, tw+=0x200, acc+=0x400.
- **FUN_18000c5e0** (double stage, slot @182616220): 0x40-chunks: load-twiddle(re,im); `FUN_180008500`(double mul, slot @182615f20) x2; `FUN_180044700`(accum).
- Dispatch-slot readers (drivers): slot cplx-mul @182615f08 read at 180001c47; stage @182616208 read at 180001e87 => runtime dispatch executors live in 0x180001xxx (thunk chain).
- DSP runtime model := plan + per-stage kernel calls (all ops = dispatched vector kernels), all through `thunk_FUN_181ba94b0` from PACE-protected callers.
- => Reimplementation path: transcribe stage-loop math (kernel shapes known), replicate plan generation, drive per-stage calls in same order; validate losses to within 0.001 dB on Reaper renders.
### Twiddle-builder FUN_180039b00 (29-fun dump, 2026-08-17) — TWO-PATH (key finding)
- arg1 = log2(N), arg2 = dest buf. Returns `buf + (N/4+1)*8`, align-down to 0x40.
- Path N <= 1024 (log2 < 0xb): copies sin VALUES directly from static DAT_182616800 (1024 doubles
= sin(k·2π/1024)), stride = 2^(10-log2N); writes buf[k] = sin(k·2π/N); terminator 1.0 at index N/2.
NO sin calls — values are precomputed in static table.
- Path N > 1024: writes ANGLE RAMP buf[k]=k·(2π/N) (two SIMD segments: forward 0..N/8, reversed
N/8..0 via vpsubd), then converts with **thunk_FUN_1801de760** (sin) and **thunk_FUN_1801e3f20** (cos).
- => loader FUN_18014ec20 (cplx-stage twiddle, 8255B) READS 16B-per-iter (2 doubles) and does
Dekker-split math (>>0xc, <<0x28, XOR masks DAT_181c91xxx, Pi-reduction with 1/π, magic 2^52*1.5)
=> it RECOMPUTES sin/cos from the angle table; it is NOT a plain copy. 3 twiddle-loader slots
182617828/850/878 ALL currently point to 0x18014ec20 (same fn).
- CONSEQUENCE: for 44100 => N=N0; if N0<=1024 the twiddle = static sin table (reproducible directly);
if N0>1024 must replicate Cody-Waite sin/cos of FUN_18014ec20 bit-exactly. N0 (+0x19c) still unfound statically.
### N0 RESOLVED - LIVE DUMP @44100 (2026-08-17) — KEY
- Soothe2 render_short.rpp (44100 Hz) + /proc/pid/mem scan of yabridge-host heap
(out file growth confirmed processing; targeted field-signature scan:
qword-aligned, +0x19c == +0x1a0 == pow2 in [256..65536], +0x1ac in {2,4,8,16}).
- Two independent live SP instances (fork L/R, vptr 0x1824ac210 / 0x1824ac7a8,
vtbl-slot families 0x18052xxx = SpectralProcessor) BOTH report:
+0x19c N0 = 2048, +0x1a0 = 2048 (=N0, since sr==44100 => 2^floor(log2(1))=1)
+0x1a4 winscale = 0.49999988 (Hann 0.5) / 0.66666639 (2/3 window)
+0x1ac oversample = 4
- N0=2048 > 1024 => twiddle goes the ANGLE-RAMP branch of FUN_180039b00
(buf[k]=k*(2π/N) then sin/cos via thunk 1801de760 / thunk 1801e3f20),
and cplx-stage loader FUN_18014ec20 re-derives re/im (Dekker-split + Cody-Waite).
- Implication for reimpl: for 44.1k we MUST reproduce Cody-Waite sin/cos
(1/π=0.31830988618379069, magic 6755399441055744, π_hi/π_lo reduction,
poly -1/3!..-1/15!), NOT the static 1024-table path. Soothing does NOT use the
static sin-table for its main FFT at 44.1k.
- Method: rtone.py (single render, wait for out growth, scan), rtver.py (field verify).
Old soothe_rt.bin is module-only range => heap objects absent (why vptr-scan found 0).
### B.1 — C++ реализация DSP-ядра (2026-08-17) — IN PROGRESS
- `dsp/fft_plan.hpp/.cpp` — radix-2/4/8 планировщик (из гигантов FUN_18002f980 и др.)
- `dsp/fft_stage.hpp/.cpp` — stage kernels FUN_18000bfc0/18000c5e0 (cplx-mul + accum)
- `dsp/fft.hpp/.cpp` — полный цикл (plan→twiddle→execute)
- `dsp/spectral.hpp/.cpp` — SpectralProcessor (STFT + детектор + mask application)
- `dsp/filter.hpp/.cpp` — FilterGraph, DigitalFilter (M10)
- `dsp/detect.hpp/.cpp` — детектор резонансов (envelope + peak suppression)
- `dsp/ms.hpp/.cpp` — mid/side encode/decode (M8)
- `dsp/cody_waite.hpp` — sin/cos (FUN_1801de760/1801e3f20)
- `dsp/phase_table.hpp/.cpp` — sin-таблица 1024
- `dsp/twiddle_builder.cpp` — FUN_180039b00 (ветка N>1024)
- `dsp/CMakeLists.txt` — сборка `libsoothe2_dsp.so`
- `dsp/harness.cpp` — WAV16→render→WAV24 (stereo+M8)
### B.2 — Сборка и верификация (2026-08-17) — IN PROGRESS
- `libsoothe2_dsp.so` — собрана
- `harness` — работает с WAV16→WAV24
- **Fixed:** WAV read aliasing bug (int16→float conversion)
- **Fixed:** In-place processing bug (separate input/output buffers)
- **Added:** Spectral detector with envelope + peak suppression
- TODO: `verify_bit_exact.py` — прогон всех свипов, побайтовое сравнение int24 PCM
- TODO: `dsp_notes.md` — документация (карта функция↔смысл, константы, LUT)
### B.3 — Осталось сделать
1. **Twiddle-loader FUN_18014ec20** — декомпил есть, нужна транскрипция (Dekker-split + Cody-Waite)
2. **Bit-reverse permutation** — FUN_18003b6c0 с XOR-маской DAT_181c5e4e0
3. **C++ транскрипция** — переписать fft.cpp с точным порядком FP-операций из декомпиляции
4. **Верификация** — подать тон/синусоиду в тот же путь, что и Reaper-рендер
### B.4 — Метрики на burst500 (2026-08-17) — BIG JUMP
- **Интеграция**: `build_twiddle` в fft.cpp теперь через `soothe::twiddle_load` (sincos_single,
Cody-Waite, 7-term poly); удалён дубль `init_plan` (конфликт с fft_plan.cpp), добавлен `<vector>`.
- **Stage-ядра** (fft_stage.cpp переписан: cplx_mul / stage_complex / stage_double = scalar mul
FUN_180008500) — починили **фазу**: corr 0.479 → **+0.995**, SNR 5.2 → **+19.8 dB** (shift 0),
амплитуда diff ±0.06 dB (ref 26.07, ours 26.13 dBFS).
- **Детектор (смена модели)**: убрана bell-boost (была НЕВЕРНА); маска soothe = **чисто
уровнезависимый floor**. Тон 258 Гц (громче) режется сильнее (−6.2 дБ), 500 Гц (3.5 дБ),
выше 1 кГц ≈ 0 — региональный уровень (RMS по окну 16 бинов, калибровка −26.1 дБFS),
`mask = 10^(1.041·depth·floor(level)/20)`.
- Итог: ref 26.07 / ours 26.13 dBFS, corr 0.99475, SNR 19.80 dB, diff 0.065 dB.
### B.5 — Детектор ДЕШИФРОВАН в soothe_mem.bin (2026-08-17) — KEY
- **Тело FUN_180535880/180536f90 в дампе = реальный дешифрованный SSE-код** (movsd/cvtpd2ps,
загрузка параметров из param_3, цикл по буферу), а НЕ PACE-стаб.
- Вызов **dispatch-shim 0x180001d00**: `mov [0x1826159a0],%rax; jmp *0x182616008[rax*8]`;
сейчас idx=4 → `0x180009860` (тонкая обёртка `e8 rel32`) → реальная **FUN_180040d40**.
- **Dispatch-таблица 0x182616008**: 223 разрешённых записи; реальные цели в `.text`
`0x18000a160..0x180074740` (13+5+8+6 функций по страницам), высокие `0x1817b900/181a69090/...`
= направляющие, не алгоритм.
- **КРИТИЧНО**: этих функций НЕТ в fun_map.txt/decomp_funs.txt (прежний Ghidra-анализ
был на зашифрованном `.vst3` и не развязал dispatch). .vst3 на диске (PE32+ 39.1M,
12 секций, base 0x180000000) СОВПАДАЕТ с дампом по байтам в `.text` (проверено
для 0x40d40/0x535880) фи(x) → декомпиляция дампа = декомпиляция реального кода.
- План: разметить таблицу → извлечь байты функций 0x18004xxxx (+0x18000a160..0x180074740)
→ objdump 0x180000000-adjust → транскрипция в dsp/detect.cpp → верификация SNR 19.8 дБ.
- Непустые ранее известные ядра (0x180040cc0..0x180041700) = 8 функций = полный алгоритм детектора.
### B.6 — ДЕТЕКТОР ПОЛНОСТЬЮ ДЕШИФРОВАН (2026-08-17) — dispatch-модель ИСПРАВЛЕНА
- **ИСПРАВЛЕНИЕ dispatch**: у КАЖДОГО стаба СВОЯ таблица (не одна общая 0x182616008).
Все стабы читают ОДИН глобальный индекс `DAT_1826159a0` (в дампе = 4, константа), но
каждый берёт свой слот из своей таблицы (RIP-rel `48 63 05/4c 8d 15/41 ff 24 c2`).
`/tmp/dispatch_map.txt` (223 записи одной таблицы) — УСТАРЕЛ для вызовов детектора.
- **Карта стабов FUN_180535880 (float-детектор)**: `0x180001d00`→tbl`0x182616008``0x180040d40`
(AVX-memset); `0x1800019a0`→tbl`0x182615b88``0x180004200``0x181a63fe0` (fill float N штук
значением из xmm0: rcx=bits, rdx=dst, r8d=N; ветка больших через 0x180017580);
`0x1800021e0`→tbl`0x182616688``0x1800130e0`; `0x1800018b0`→tbl`0x182615a48``0x180003040``0x181a557c0`;
`0x180001af0`→tbl`0x182615d48``0x180005a20`; `0x180001dc0`→tbl`0x182616108``0x18000ad60`
(комплексное умножение `vfmaddsub213ps`); `0x180001fd0`→tbl`0x1826163c8``0x18000f620`
(векторная FMA-аккумуляция `out[i]=out[i]+xmm1*in[i]`, AVX2, развёртка по 8);
`0x180001e20`→tbl`0x182616188``0x18000b820``0x181a775a0`.
- **FUN_180536f90 = DOUBLE-БЛИЗНЕЦ FUN_180535880** (тот же 3-итерационный цикл, `movsd`/`shl 4`
против `movss`/`shl 3`): `0x180001a30``0x180004d60`; `0x1800022d0``0x180015300``0x180040d40`;
`0x180001e80``0x18000c7e0`; `0x180001910``0x1800034c0``0x181a62d00`; `0x180001ac0``0x180005720`;
`0x180001e50``0x18000bba0`; `0x180002150``0x180012320` (double-FMA-аккум);
`0x180001f40``0x18000df20``0x181a78060`.
- **Алгоритм FUN_180535880** (rcx=this, rdx=r15=out, r8={double* A, double* B} по 3 double,
r9→[rsp+0x20], [rsp+0xf0]=N): читает A[0..2]/B[0..2], cvtpd2ps→стек [0x28..0x40];
r13=2N, r12=r15+N*8, rbp=r15+2N*8. Цикл i=0..2:
- i=0: memset r15 (N qword) значением (A0,0); memset r12 (N qword) значением (B0,0);
- i=1: rbx=r15+3N*8; fill rbx (N float) константой 0x1824c3ea4; затем 0x1800021e0(rbx,r9,this,N),
0x1800018b0(this,N), 0x180001af0(this,rbp,N);
- i=2: 0x180001dc0(this,rbp,rbp,N) = комплексное умножение rbp на самого себя;
- (после каждого i): rbp += A[i]*r15 (0x180001fd0, 2N float) и rbp += B[i]*r12;
- после цикла: 0x180001e20(r12,r15,this,N).
- **Ключевые ядра (реальные .text/0x181a*)**: `0x180040d40`=AVX memset по qword-паттерну (798б);
`0x181a63fe0`=fill N*4 байт широковещательным 32-бит значением (AVX2, up to 0x8000);
`0x18000f620`=FMA-аккум (scalar+ymm+tail, возврат -8/-6 при null/0);
`0x18000ad60`=комплексное умножение (vfmaddsub213ps, цепочки xmm/ymm, маска-зоны 0x20 байт);
`0x180005a20`=обёртка больших chunk`ов (0x7fffffe0 по 8байт) вокруг 0x18001a5a0.
- **Конечные артефакты**: `/tmp/det/det_535880.bin/.dis` (1668б, 435 стр), `det_536f90.bin/.dis`
(1935б, 582 стр), `r_*/f_*/p_*` для всех ядер, `sizes.json`.
- **Следующий шаг**: транскрипция обоих близнецов в dsp/detect.cpp (или dsp/detector.cpp)
с точным порядком float/double операций + комплексного умножения rbp; верификация SNR 19.8 дБ.
### B.7 — Коэффициенты детектора/внутренние моды band (2026-08-18) — ДЕШИФРОВАНО
- **Ghidra-путь Layer2**: `analyzeHeadless ... -process soothe_mem.bin -noanalysis -scriptPath D -postScript X.java`
(~60-90 с/запуск), `-noanalysis` ОК; image_base=0, но block `ram`=[0x180000000,0x186f48fff] =>
адреса = RVA; `A(v)=v` (не минус base). Старые `.py` не работают (нет PyGhidra) → только `.java`.
Побочный баг: скрипты в одной папке перестают компилироваться после одной правки (OSGi кэш по пути) —
обход: новый каталог на скрипт. Материалы: `/tmp/opencode/ghidra_out/*.java`+`qmode.txt/q2.txt/q3.txt/
consts.txt/swtab.txt/scanaddr.txt`, копии в `/tmp/det/ghidra/`.
- **band-структура (подтверждена сеттерами)**: `+0x800`=int mode (сеттер 0x18052d206: пишет XML value),
`+0x804`=freq float (0x18053763b), `+0x808`=sens float, `+0x80c`=**Q float** (0x180537615),
`+0x814`=order count (2/3/5), `+0x818/+0x820`=ptr, `+0x819/+0x81a`=byte флаги.
- **Писатель коэффициентов `FUN_1805316e0` (0x1805316e0..0x180532f3d)**: switch на `[band+0x800]`,
jump-table 0x180532f80 (база LEA 0x180000000, 17 случаев 0..16, >0x10→JA default), каждый case:
order в `+0x814`, вызов генератора, memcpy A/B→band.A/B (0x181132884, len `[+0x10]<<3`).
case1 (XML mode=1)=`FUN_1805343e0(fs,freq,Q=0.707)` — **фиксированный Q**, `+0x80c` НЕ читается!
case8 (default)=`FUN_180533ec0(fs,freq,Q=[+0x80c],10^(sens/20))` — полноценный RBJ bell, Q из поля.
Случаи 0/2/3/4/9..16 — другие типы (shelf/notch с константами 0.54/0.51/0.6/1.31/2.56...).
- **q-фактор из `+0x80c` на старте** (0x180531771..17f1): `2q²; (2q²+1)/2q²; ·2; ²·0.251;
(cf4); +XMM6; (cd0); ·100000; (cc4 on 2.0); /100000` → участвует в клипинге частоты и fVar16.
- **Генераторы (decomp3.txt)**: `FUN_1805343e0` w=1/FUN_181a14cfa((freq·π)/fs), k=(1/Q)·w,
a=1/(k+1+w²), A=[a,2a,a], B=[1,(1w²)·2a,(1k+w²)·a]; `FUN_180533ec0` RBJ bell с gain
(f=(float)p5, w0=clamp·2π/fs, alpha=(cos·0.5)/Q, a0=alpha·f+1, b0=alpha/f+1, b1=s·−0.5);
FUN_180534300 (3rd-order вариант), FUN_180533ff0/180534180 (1-3 order bell/notch).
- **Константы (consts.txt)**: (float) 0x1824c41e0=2.0, 0x1824c3ea4=1.0, 0x1824c3d3c=0.25,
0x1824c45f8=100000.0, 0x1824c3e00=0.707, 0x1824c3d8c=0.5; (double) 0x1824c43d0=100000.0,
0x1824c40c0=0.707, 0x1824c4698=0.5, 0x1824c4f20=0.0, 0x1824c4270=10.0, 0x1824c42b0=20.0.
- **Матем. хелперы 0x181a14xxx — IAT-стабы** (`JMP [0x181bab3xx]`), цели 0x6fffff... вне дампа
(грузит плагин). Семантика `FUN_181a14cfa` не подтверждена: кандидаты sin/sqrt/tan —
проверяется на харнессе против baseline (corr 0.99475/SNR 19.80/diff 0.065).
- **ОТВЕТ НА ВОПРОС (c): XML mode=1 реально исполняет case8 (mode=8/default), НЕ case1**.
Доказательство — серия `dual_b1q_*` (тон 500+2000, band1 на 500, sens=12, mode=1):
подавление @2000 (red@500≈10.2 насыщена и q-независима) монотонно убывает с q:
q=0.1→+5.00 dB, 0.3→+2.73, 0.5→+1.5, 0.7→+0.9, 1.0→+0.47, 1.5→+0.09, 2.0→−0.04,
3.0→−0.06. case1 (фикс Q=0.707) q НЕ читает → исключён. case8 (Q=+0x80c, fvar=0.6=sens/20):
delta(2000)=[4.94,1.9,0.85,0.46,0.24,0.11,0.06,0.03] rmse≈0.42, верный знак/направление
(завышен при q≥0.5 — модель шире факта). Свободный Q_eff(q): ratio≈0.70.95 при q≤1,
→ рост при q≥1.5 (Δ<0 — вне bell). Структура `FUN_180533ec0` подтверждена.
mode4-путь (Q=0.5 фикс, fvar=(q0.707)/2+1) даёт ОТРИЦАТЕЛЬНЫЙ тренд (delta 0.57→−5.45) —
несовместим. Вывод: case8-структура (A=[1+α,2s,1−α], B=[1+α2,2s,1−α2], α=p·f, α2=p/f,
p=0.5·cos(w0)/Q, fvar≈sens/20 или sin(sens/20)=0.565) — модель формы полосы; fvar-различение
sin vs x — на харнессе (Q_eff ratio 0.96/0.70 vs 1.25/0.75 при q=0.1/1.0).
Несоответствие формы t1kq_only (D(800)=0.92 при Q=0.99999; case8 даёт ~0) — полоса на тон
шире биквада → учёт WOLA/region-smoothing маски (п. B.4) + Q_eff-маппинг — следующий шаг.
### B.8 — Форма полосы case8: КОРРЕКЦИЯ (middle=-2c) + freq-axis маска (2026-08-18)
- **ИСПРАВЛЕНИЕ B.7**: в `FUN_180533ec0` средний коэффициент = **-2·cos(w0)**, НЕ -0.5·sin(w0).
В B.7 перепутаны c/s. Правильная форма (проверено, rmse=0.007 dB на t1kq_only1):
`w0=2π·fc/fs; c=cos(w0); s=sin(w0); p=(s·0.5)/Q; alpha=p·gain; alpha2=p/gain;
A=[alpha+1, -2c, 1-alpha]; B=[alpha2+1, -2c, 1-alpha2]; mask(f)=|2·B(e^-jw)/A(e^-jw)|`.
Свойства: |2B/A|=2 (+6.02 dB) на DC/Nyquist, NOTCH на fc (gain>1), ширина ~1/Q.
- **Freq-path (FUN_180530850)**: близнец вызывается НАПРЯМУЮ `FUN_180535880(out=0x540708,
buf=0x5406f8, coeff=case8(fs, 8000.0, Q=1.0), in=0x540718, N=513)`, N=NFFT/2+1;
затем copy 0x540708→0x5406f8 (0x1805355d0) и `×=` warp-axis 0x5406a8 (0x18052d990).
warp-axis 0x5406a8[i] = x/(1+x/f9), x=f_bin/2000 (f_bin=i·fs/1024), f9=cdc().
=> **mask(f) = |2·B/A|(z=rotor(входная ось)) · (f/(f+2000)-warp)**.
- **Level-path (FUN_18056e3e0, per-band)**: normalize вход × 2π/(os·sr) (0x180006e40 = обычный
скалярный mul, НЕ frequency-ramp — проверено декомпиляцией), близнец на буфере 0x400,
product активных полос → out. Вход в FUN_180563440 = LUT-рампа [0,1] (шаг 1/1023) из
кривой param_1+0x188 → это УРОВНЕВАЯ LUT-маска (B.4), не аудио.
- **Численные согласия**: t1kq_only1 rmse=0.007 (Q_eff≈1.39-1.48, gain≈1.19-1.2); t1k_b1f
Q_eff≈1.03, gain≈1.39; t1kq_b1f Q_eff≈1.11, gain≈1.29. sens=12→gain≈1.2-1.4 (НЕ 3.98).
Глубина вреза = f(уровень входа) → задаётся level-путём; форма (Q_eff, notch) — freq-путём.
- **ОТКРЫТОЕ ПРОТИВОРЕЧИЕ dual_b1q**: red@500≈10.2 const, red@2000=15.22→10.16 (q=0.1→10).
red2000>red500 при q<1 НЕВОЗМОЖЕН для любой маски-нотча |2B/A|(·warp) (врез=минимум, warp
f/(f+K) усиливает cut на 500). Все 4 rotor-мода + warp дают red2000<red500. => В dual есть
второй механизм (спектральный наклон/уровень-маска per bin: 2000 Гц выше по f/(f+2000)>0.5
→ нормированный уровень выше → глубже крас). Нужен full-харнесс обоих путей.
- **Константы**: DAT_1824c4380=8000.0 (double, фикс-freq default-генератора), DAT_1824c45b4=2000.0,
DAT_1824c4110=2.0, DAT_1824c3e50=0.87, DAT_1824c3c54=1/1023 (LUT-шаг), 0x180006e40/0x180004720=
double/float pointwise scale, 0x1805355d0=copy, 0x18052d990=double pointwise mul.
### B.9 — Level-path ЗАМКНУТ: потребитель per-bin весов + warp-ось sigmoid (2026-08-18)
- **ПОТРЕБИТЕЛЬ НАЙДЕН**: per-bin веса 0x5406b8/6c8/6d8/6e8 применяются в **FUN_180529fe0**
(per-block спектральный применитель, 5 каналов, ключевой DSP-цикл). Для каждой полосы:
1) `0x540678[band] *= (amp/sr)·0x540870` (уровень×sens); 2) IIR-сглаживание спектра
(состояния 0x540528/2c04f8/3404f8/4c0528/440510 — per-bin level-трекеры); 3) резонанс
полосы в 0x5406f8 (каскад 0x540688-коэфф); 4) `0x5406f8 *= 0x540698 (freq-ось) *= 0x5406a8`;
5) **`0x5407c8[band] += 0x5406c8[i]·contrib` и `+= 0x5406e8[i]·contrib`** (0x180003c40);
6) накопление с 0x540678[band]; 7) dry/wet `0x1c[band]`; 8) FFT-свёртка в time-domain
(FUN_180535a70, FFT-таблицы 0x540548/550/598, out 0x540668).
- **Точная формула весов (FUN_180530d30)**: `iVar6=NFFT/2+1; fVar12=2000/(0x24·0.5)=0.1`
(p24=40000 из конструктора); `fVar9=cdc(51.3/(i+1))` (=exp(51.3/(i+1)));
`fVar11=0x540880·0.25·fVar9·fVar13` (fVar13=4); `w=log10(0.1, 1/(exp(1/(1+fVar11/(4·0x540880))·fVar11)·dVar1))`.
Численно: bin500 w≈2.49 (1-w=-1.49), bin2000 w≈1.60 (1-w=-0.60) — частотно-убывающие веса.
- **warp-ось 0x540748 НЕ f/(f+K)**: `buf[i]=8.3-7/(1+exp((min(i/n,1)·20000-120)·(-0.01)))`
≈ 1.3 (плоско, DC-буст 6.68 на бине 0). Гипотеза warp=f/(f+2000) из фитов — ОТМЕНЕНА.
- **Частотная ось 0x540698**: log-интерполяция (0x5406f8 ×= 0x540698). Фактическая freq-
селективность маски = |резонанс|(bin) × freq_axis(bin) × level_weight(bin).
- **FUN_180535ae0=конструктор**: 0x540874=1.0, 0x54087c=0.5, 0x540884=1.0, 0x54088c=1.0,
0x540894=1.0, **0x24=40000.0f**, 0x5408ac=0x01000000. FUN_180535f10=деструктор.
- **МОДЕЛЬ dual_b1q (закрыта структурно)**: mask(bin)=|резонанс_полосы|(bin)·freq_axis(bin)
·level_weight(bin, 2000/f_bin). На 2000 Гц level_weight иная, чем на 500; при низком Q
резонанс шире → вклад 2000-бина растёт → глубже крас. Численная проверка — следующий шаг
(standalone-харнесс FUN_180529fe0+530d30+твин).
- **Декомпиляции**: /tmp/consumers_out.txt (FUN_18052e9b0, FUN_180529fe0, FUN_180535ae0,
FUN_180535f10), /tmp/twin_out.txt (близнецы 0x180535880/0x180536f90 — 3-фазные драйверы),
/tmp/spec_out.txt (FUN_180529ef0=deferred-обновление параметров), /tmp/det/level_notes.md.
### B.10 — ЧИСЛОВАЯ ВЕРИФИКАЦИЯ МОДЕЛИ: парадокс dual_b1q РЕШЁН (2026-08-18)
- **Единая модель** (совместный фит 31 точки, rmse=0.109 dB):
`red(f) = -20*log10(1 - C(f))`,
`C(f) = depth · tilt(f) · D0 · (L0/res(f))^p`,
`res(f) = |2·B/A|(f; fc, Q, gain)` (case8/m2c, freq-path),
`tilt(f) = 1 - w(f)` (per-bin level-вес из FUN_180530d30),
`depth = 0.86397`, `L0 = уровень входа (линейный)`, `gain ≈ 10^(sens/20)`.
- **Параметры фита**: Q_eff=0.900 (vs истинный q=1.0), gain=4.132 (≈sens12=3.98),
p=0.0847 (компрессивный level-LUT), D0=0.505,
tilt: 500=1.414, 1000=1.454, 2000=1.795 (ratio 2000/500=1.269).
- **РЕШЕНИЕ ПАРАДОКСА dual_b1q**: red2000>red500 НЕ резонансом, а частотным НАКЛОНОМ
tilt(f)=1-w(f) (растёт с частотой). Резонанс входит как (L0/res)^p — "excess над уровнем
полосы" (на центре res минимален → excess макс → глубже врез). При низком Q res(2000)
мал → excess(2000) велик → крас глубже; при высоком Q res(2000)→2 → excess падает → крас
→ уровень red500. Всё сходится.
- **Согласованность с декомпиляцией**: tilt ratio 1.27 точно воспроизводится формулой
FUN_180530d30 при L=0.429, d1=0.078; gain=4.13 ≈ 10^(12/20); Q_eff≈0.90 близок к истине
(в B.8 сырой |2B/A| давал 1.39-1.48 — модель B.10 физичнее).
- **Проверки модели**: dual_b1q rmse=0.127, t1kq fc-скан rmse=0.047, joint 0.109.
Уровневая зависимость (t1kq 18dB vs dual 7dB) в одних параметрах через L0.
- **Файл**: `/home/m/re-tools/model_dual.py` — канонический совместный фит.
### B.11 — LUT-КРИВАЯ "уровень->маска" ИЗВЛЕЧЕНА: ЕДИНАЯ для всех уровней (2026-08-18)
- **НОВЫЕ ДАННЫЕ**: серия `t1k_b1f` (тон 1к, **0 dBFS**, q=0.9999978, sens=12) — третий
уровень входа; fc-скан 800..1200 даёт red 14.5..15.6 dB (глубже dual/t1kq). Параметры
идентичны t1kq_b1f (проверено по XML) — различие только в уровне тона.
- **НАДЁЖНЫЙ ЗАМЕР**: 1с-окно Hann + компонент-корреляция (проекция на cos/sin 1к) —
стабильный steady-state; даёт в точности T1KQ-bandshape (8.788 на 1000) и НОВЫЙ T1K
(15.626 на 1000). Прежний 10мс-зонд был зашумлён (осацилляции маски ~4.5Гц, боковые
±5Гц у тона — spectrum-распад/модуляция).
- **КОЛЛАПС (главное открытие)**: все 36 точек (dual_b1q 22 + t1kq 7 + t1k 7, уровни
-7.14/-18.06/0 dBFS) ложатся на ОДНУ монотонную кривую
`C_norm = LUT(x)`, `x = log10(L0 / res(f))`,
где `res(f)=|2·B/A|` case8/m2c (Q=0.900, gain=4.132), C_norm=(1-10^(-red/20))/(depth·tilt).
Спред внутри бинов 0.002-0.006 (≈0.1 dB) — форма резонанса ОДИНАКОВА на всех уровнях.
- **Форма LUT**: монотонная, асимптоты y→0.44 (низкий уровень) и y→0.67 (высокий),
с крутым "коленом" при x≈0.58 (dual@500: y=0.566 при x=0.574 vs t1k@800: y=0.647
при x=0.608). НЕ степенной закон: наклон d(ln C)/d(dB) падает с уровнем
(p_eff≈0.19 при 0dB, ≈0.089 при -13dB) → степенная C∝L0^p НЕ работает на 0dB
(давала бы 12.4 вместо 15.6).
- **ВЕРИФИКАЦИЯ**: непараметрическая PCHIP-LUT (10 узлов) + форма B.10 →
**rmse=0.0718 dB** на всех 36 точках (dual 0.083, t1kq 0.058, t1k 0.036).
Остаточные выбросы ≤0.1 dB.
- **Структурное соответствие**: LUT = кривая param_1+0x188 из FUN_180563440
(`val = min+(max-min)·x^(1/gamma)`, linear flag) — именно gamma/насыщающая кривая,
а не степенная. Вход = per-bin уровень (IIR-трекеры 0x563ce0) нормированный,
x = L0/res = превышение уровня над резонансной реакцией.
- **Итог B.10→B.11**: парадокс dual_b1q полностью объяснён (tilt×резонанс×LUT);
уровневая зависимость = LUT-кривая, НЕ p-степень; форма резонанса (Q,gain,tilt)
стабильна для 3 уровней. Модель закрыта численно до бит-экзакта.
- **Файл**: `/home/m/re-tools/model_lut.py` — КАНОНИЧЕСКАЯ модель B.11 (узлы LUT,
red(), rmse). Вспомогательные: fit_level.py (отказ p-степени), fit_lut*.py
(логистика/gamma — хуже), extract_lut.py (коллапс), verify_lut*.py (PCHIP-фит).
### B.12 — Bridge-модель + диагностика уровня (2026-08-18, handoff §5)
- **Модель** (36 точек, rmse=0.236 dB): `C(f) = g·LUT(log10(L0/res(f))) + w·warp(f)^a`,
g=1.221, w=0.358, **a=3.143≈π** (кратный каскад freq-axis ∏0x540688/FFT-conv).
- **Модель аддитивная**, НЕ мультипликация warp·LUT (та проваливается >10 dB).
warp-терм даёт +0.15 на 2000 Гц и ≤0.02 на 1000 (иначе t1k рушится).
- **FFT-conv полностью размаплен** (0x52b550..0x52b8b5, plan 0x540530, окна 0x540548/550/598);
freq-axis 0x540698 = OFFLINE константа (exp/rand = online-дитер).
- **Остаток**: 0.7 dB при Q=0.1 — форма LUT-колена x≈0.574, не закрывается per-bin.
### Phase 5 — dB-parity на реальных wav (2026-08-18, commit f96bb42)
- **`render_parity.py`**: Goertzel steady-state замер прямым корреляционным детектором
(см. handoff §5; трим по моно-сумме каналов).
- **Результат** (канонический B.12, 36 точек): **TOTAL rmse=0.268 dB**
(dual500 0.130, dual2000 0.387, t1kq 0.320, t1k 0.076); рефит (g,w,a)=(1.224,0.379,3.405)→0.264,
+2 LUT-узла→0.203 (плато). Остаток структурный (0x540658 FFT-шейпинг + level-область).
### B.13 — res_power + полный декомп LUT/IIR (2026-08-19, commits 627e837/9542201/a909aae)
- **res_power breakthrough**: `gain = (1C) · res^rp` решает 500Hz-residual для q0.1/q1
(envRmse q0.1@500 0.64→0.05 dB). Источник — res-зависимый gain в res-weighted пути
в twin-mask factory FUN_18056e3e0.
- **FUN_180563440 декодирована** (222 строки): 3 фазы/фрейм:
1) 1024-bin LUT loop (`x=k·0.0009775`, кривая +0x188: min/max/gamma/flag/nodes);
2) twin-mask factory FUN_18056e3e0 (6 полос × 1024 бин, stride 0x2000, combine 1−Σ);
3) combine loop с 0x198 (double, stride 8).
Paths: callback-dynamic / **power-law** `sign(x)·10^(log10|x|/threshold)` (flag=1) / **linear** `(BA)x+A`.
- **FUN_180563ce0 декодирована** (ИНИТ per-bin IIR level-трекеров): 341 бин, order-3,
α=0.1, инициализация [1,0,0,0]/[1,0,0,0]; UPDATE-loop — в другом месте.
- **FUN_180529fe0** (коэффициенты): PRNG-LCG, depth `powf(normalized, depth)`, mask 1C.
- **Константы из дампа**: SCALE=1/1024 (0x24c3c54), ONE=1.0, TWO=2.0, NEG1=1.0, HALF=0.5,
DEPTH_SCALE=4.0, DB_CONV=8.6859, FLOOR=6.9078, IIR α=0.1.
- **G/W/A/rp=1.0066/0.3261/1.0764/0.0169** — dual-only (mean=0.160 по СТАРОЙ метрике).
### B.14 — Q-dep rp + ЧЕСТНАЯ метрика (2026-08-19, commit 28cd3b7) — артефакт-коррекция
- **АРТЕФАКТ**: ранние fit-скрипты рендерили выход длины `nfr·HOP+N` (не тримнутый),
tone-замер шёл по последним 0.75s этого расширенного буфера → систематический сдвиг ~+0.2 dB.
Честный (trimmed, длина=len(x)) базлайн для scalar rp=0.0169 = **0.280**, НЕ 0.160.
- **Q-dependent rp** (физический warp сохранён): `rp(Q)=rp0·Q^drp`, G/W/A/rp0/drp =
0.9963/0.3335/0.9807/0.0275/0.2159 → **trimmed mean=0.175**, max=0.707:
q0.1: +0.000/+0.001 · q1: 0.256/0.707 · q10: +0.086/+0.001.
q0.1 и q10 почти идеальны; боттлнек q1@2000 (over-reduction).
- Pефрактор: weighted-фиты ошибку перекладывают (q1@20000.71→q0.1@2000+0.89), max не идёт ниже 0.7.
- Free-warp (8 параметров, alpha=3.19): mean=0.139, но warp НЕ физичен и q1@2000=0.53 — отброшен.
- **Спектральные веса FUN_180530d30 — повторно NEGATIVE при N=2048** (наш рендерный N):
w(500)=w(1000)=w(2000)≈10⁻⁴⁰…10⁻²⁰⁰ → 1w→1.0, тилта 2000-vs-500 нет (подтверждает вердикт 2026-08-18).
### B.15 — Диагностика LUT-form + JOINT free-knot LUT (2026-08-19) — dual прорван
- **Диагноз q1@2000**: наш Pchip-LUT **немонотонен** (провал 0.366@xv=0.5012 → пик 0.4552@xv=0.5);
замеренная al_*-референс-кривая монотонно убывает, at xv=0.419 LUT_ref=0.362 vs ours=0.457
**dC=0.095 (over-reduction)** — ровно зона q1@2000 (xv≈−0.57).
- **FIX: совместный фит LUT-узлов** (8 узлов Pchip, KX=[0.8..1.0]) + G/W/A/rp0/drp
по dual (6 точек, trimmed-рендер) + al_* (6 точек, прямой C-замер):
**G/W/A = 0.9752/0.3394/1.0222, rp0=0.0254, drp=0.2231; LUT строго монотонен.**
**dual errs: q0.1 +0.07/0.00 · q1 0.06/0.00 · q10 0.00/0.00 → mean≈0.027 dB!!**
(q1@2000 закрыт до 0.000; единственный остаток q0.1@500 +0.07).
- Cross-check с фиксированной измеренной al_*-LUT (без свободы узлов): dualmean=0.081,
но al_* dC до +0.16 (узлы не совпадают с dual-поведением) → свободные узлы нужны.
- **Остаток**: al_* lv24 dC=+0.10 (наша LUT всё ещё завышает на xv<−0.3).
Следующий шаг: form-констрейн (линейный/мультисегментный LUT без провалов) или
структурная runtime-кривая FUN_180563440 вместо Pchip.