Files
soothe2-re/roadmap.md
T

214 lines
19 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).