Files
soothe2-re/roadmap.md
T

427 lines
40 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` (прогон всех свипов, побайтовое сравнение).
## Задел (уже есть)
- 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`.
## Контракт из 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 (~40%)
- **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**: свой планировщик есть только у Vorbis-декодера; DSP-STFT-FFT ещё не найден (A.3) —
если свой radix-2 — воспроизводим напрямую; главный нерешённый риск фазы A.
- **Хост-зависимость**: уточнить точный размер буфера/фрейма (рендеры при разных RENDER_RANGE должны давать одинаковые байты).
- **Stereo**: все текущие свипы mono; для бит-exact графа M8 нужны стерео-рендеры (link/balance/ms).
## Открытые вопросы
1. Где реальный DSP-STFT FFT (не Vorbis/не внешний lib)? Искать большие функции с butterfly/стадиями
в 18052*/18053* или через трассировку вызовов от `processBlock` спектрального процессора.
2. Окна/oversample детали — из декомпиляции (A.6) или требуется доп. замеры.
3. Стерео-верификация (M8) — приоритет mono-путь или сразу стерео-граф.
## 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` — канонический совместный фит.