# 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-ядра — закрыт (~95%)**: twin-резонатор, генератор case8, level-path (0x530d30/0x529fe0), LUT-кривая (FUN_180563440), IIR-трекеры (FUN_180563ce0), FFT-conv (0x535a70), main render-loop (FUN_18052e260) декодированы; `.dis` в `handoff/nls_dasm/`. Остаток — runtime-значения (коэффициенты IIR level-tracker 0x4c0528/0x3c0510/0x2c04f8, скаляры 0x540870/874/87c/888/88c из RPP, цель P1.5 live-capture). - **Численная модель = frame-рендер `framed_render.py`** (STFT N=2048, hop=512, sqrt-Hann, twin-env tatt=11ms/trel=80ms): `C(f)=G·LUT(xv)+W·warp(f)^A`, `gain=(1−C)·res^(rp0·Q^drp)`. - **Честная тримнутая метрика** (длина выхода = длина входа): Q-dep rp → **mean=0.175 dB** (q0.1/q10 идеальны, боттлнек q1@2000=−0.71); joint free-knot LUT (B.15) → **dual mean≈0.027 dB**. - **Цель bit-exact (Phase C) — в работе**: twin-транскрипция в `dsp/` готова и проходит float-parity (1.27e-5); **живые таблицы встроены** (P1.1, tables_check ALL OK); **FFT-conv расшифрован** (P1.2: f_52d990 = memcpy, N=4096) и **интегрирован** (P1.3 fftconv.cpp); **маска-канон подтверждена** (P1.4, FUN_180529fe0) и **level-tracker UPDATE реализован** (P2, leveltrack.cpp). Остаток кода: twin-mask factory (FUN_18056e3e0), mask-accumulator 0x5407c8, render-loop 52e260, бит-экзактная base-CRT FFT, стерео M8, multiband combine 5316e0, + live-коэффициенты (P1.5). res_power/Q-dep-rp порт в C++ — ещё TODO. - **ВАЖНО про метрику**: ранние записи "mean=0.160/0.144" считались на выходе НЕтримнутой длины (`nfr*HOP+N`) и были артефактом; честный базлайн для scalar `rp=0.0169` = **0.280**. Все числа в этом файле — на ТРИМНУТОЙ метрике. ## Модель в двух строках ```python xv = log10(A_k / res_k) # A_k = 2|X_k|/wsum (twin-env), res = |2B/A| case8 C = G * LUT(xv) + W * warp(f_k)**A # additive mask (НЕ мультипликация warp·LUT) gain = max(1 - C, eps) * res_k**rp # rp = rp0 * Q**drp (res_power) ``` - LUT — Pchip; warp — `0.87·7.942·x/(7.942+x)`, x=f/2000 (аппроксимация runtime-вычисления FUN_180563440); - Q-dep rp: `rp0=0.0275, drp=0.2159` (mean 0.175); B.15 joint-free-knot LUT: G/W/A/rp0/drp = `0.9752/0.3394/1.0222/0.0254/0.2231` (dual mean 0.027). - Запуск канона: `python3 framed_render.py dual`. ## Статус по фазам - **Фаза 0** — сделано (OCR стр.30 manual → `pipeline_ocr.txt`, топология ниже). - **Фаза A (статический RE)** — закрыта ~90%. Карта «что декодировано → где детали»: | Компонент | Функция | Детали | |---|---|---| | Twin-резонатор | FUN_180535880/536f90 | `handoff/NOTES_TWIN.md`, `dsp/twin.*` | | Генератор case8 | FUN_180533ec0 (RBJ bell, middle=−2cos w0) | `handoff/SESSION_HANDOFF.md §4` | | Writer коэффициентов | FUN_1805316e0 (17 case, mode→тип полосы) | `NOTES_LEVEL.md`, B.7 | | Level-weight | FUN_180530d30 (численно НЕ даёт tilt) | `NOTES_LEVEL.md`, B.9/B.14 | | LUT-кривая | FUN_180563440 (linear/power-law, 1024 бин) | `handoff/nls_dasm/f_563440.dis`, B.13 | | IIR level-трекеры | FUN_180563ce0 (init: 341 бин, α=0.1) | `handoff/nls_dasm/f_563ce0.dis`, B.13 | | Mask-применитель | FUN_180529fe0 (PRNG, depth, mask 1−C) | `handoff/nls_dasm/f529fe0.dis` | | FFT / twiddle / Cody-Waite | FUN_180008140 / FUN_180039b00 / 1801de760 | `notes_giant_fft.md`, `dsp/fft*` | - **Фаза B (численная модель)** — пройдена до B.15: от B.10 (rmse 0.109 dB) до full frame-рендера Q-dep rp (0.175) и joint free-knot LUT (dual 0.027). Канон — `framed_render.py`. - **Фаза C (C++ bit-exact)** — in progress: `dsp/twin.*` готов и проходит float-parity (1.27e-5); `dsp/{tables_data, fftconv, leveltrack}.cpp` + `dsp_ctx.hpp` готовы (P1-P2 структурно); `verify_bit_exact.py` даёт базлайн (все DIFF — ожидаемо до подключения живой маски). Оставшийся код по фазам: | Фаза | Компонент | Статус | |---|---|---| | P1.5 | live-коэффициенты level-tracker A[] + скаляры RPP | ✅ DONE (rtctx_live.json, leveltrack_data.hpp) | | P2 | mask-accumulator combine kernels (8d60/5a20/3c40) | ✅ DONE (levelpath.cpp combine_sub/acc_add/acc_fma) | | P2.5 | twin-mask factory (56e3e0) + band LUT apply (563a60) | ✅ DONE (levelpath.cpp) | | P3 | бит-экзактная base-CRT FFT | ⚠️ partial: архитектура размечена, vlog(ln) транскрибирован; butterfly+shuffle+plan-gen осталось | | P4 | render-loop 52e260 + связка mask→FFT-conv + full pipeline | TODO (структура известна, компоненты готовы) | | P5 | стерео M8 (link/balance/LR-vs-MS) + multiband combine 5316e0 | TODO (в скоупе, не начат) | Обновлённая оценка (2026-08-20): структурно ~65-70% готово — все level-path компоненты (LUT 563440, twin-mask 56e3e0, band LUT 563a60, combine, level-tracker, FFT-conv, vlog) транскрибированы и проверены *_check. Осталось: (а) полная бит-экзактная FFT (butterfly+shuffle+ plan-gen — недели), (б) сборка единого pipeline P4 (render-loop + связка), (в) стерео M8 (P5). ## Ключевые находки (итог) - **Маска аддитивная**: `C = g·LUT(xv) + w·warp^a`, НЕ мультипликация warp·LUT (та проваливается >10 dB). warp-terм ≈ `warp^3.14·w` — кратный каскад freq-axis ∏0x540688/FFT-conv. - **res_power**: `gain = (1−C)·res^rp` — закрывает 500Hz-residual для q0.1/q1 (envRmse 0.64→0.05 dB). - **0x530d30 дважды NEGATIVE**: спектральные веса численно ≈0 (10⁻⁴⁰…10⁻²⁰⁰) → тилт 2000-vs-500 из них НЕ идёт (подтверждено при N=1024 и N=2048). - **Форма LUT**: Pchip-эмпирика была немонотонна (провал при xv≈−0.5); настоящая кривая FUN_180563440 — гамма/монотонная; при замене на монотонную (free-knot или структурную) q1@2000 закрывается до 0.000. - **Константы из дампа**: SCALE=1/1024, 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, LCG_OFFSET=0x3cdca. ## Контракт из manual (`soothe2_ManualFAQ.pdf`, v1.0.0) | # | Факт | Следствие для DSP | |---|---|---| | M1 | Soft/Hard: режим меняет всё; depth 3.0 в soft ≠ hard (стр.7) | mode = глобальный препроцессор детектора | | M2 | Depth — референтный dB (−18..18), реальная глубина до 60dB (стр.8) | вход depthcurve-LUT; clip 60dB | | M3 | Sharpness: выше = глубже и уже (стр.8) | Q_notch растёт с sharp | | M4 | Selectivity: 0 = «everything must go» (стр.9) | отбор/порог пиков, число нотчей НЕ лимитирует | | M5 | Attack/Release: реальные времена частотно-зависимы (стр.11) | Env НЕ один one-pole; нужен частотный масштаб | | M6 | Oversample = расчёт reduction-фильтра в высшем разрешении (стр.11) | интерполяция детекторной сетки → фильтра | | M7 | Resolution = частота обновления детекции/фильтра (стр.11) | период пересчёта коэффициентов | | M8 | Stereo: link 100% = сумма каналов; 0% = dual mono; balance; L/R vs M/S | детектор на sum; коррекция симметрична/раздельна | | M9 | Trim регулирует ТОЛЬКО wet; Delta = x−wet; Mix 0..100% (стр.14) | порядок: detect→notch→trim→mix/delta/bypass | | M10 | Банды: 2 cut + 4 general; general: peak/shelf/reject/tilt (стр.17-19) | `FilterGraph`, `DigitalFilter` мультитипный | | M11 | Кривая = inverse EQ, сумма band-кривых = белая (стр.17) | `red(f)=base+ΣW_i·H_i` | | M12 | Sidechain; Input trim — скрытый предусилитель анализа (стр.26) | отдельный вход/гейн анализа | ## Топология пайплайна (Приложение A, стр.30 — `pipeline_ocr.txt`) ``` main inputs ──┐ sidechain ────┤ (sidechain enabled?) ▼ input trim ──► analysis (детекция; depth/sharpness/selectivity; EQ из per-band balances) │ stereo apply + balance (channel link + global balance) │ ms encode? ──► processing (нотч-синтез в M/S) ──► ms decode? │ mix ──► bypass? ──► output ``` - mid/side ручка: обработка в M/S-области — стерео-путь подтверждён диаграммой. - GUI/visualisation — побочная ветвь, в DSP-ядро не входит (кроме параметров). ## Журнал вех (детали — в handoff/ и git log) | Веха | Дата | Суть / ключевые числа | |---|---|---| | B.1 | 08-17 | C++ каркас `dsp/*` (twin, spectral, filter, detect, ms, fft, cody, twiddle, harness) | | B.2 | 08-17 | `libsoothe2_dsp.so` + harness WAV16→WAV24; фиксы WAV-алиасинга / in-place | | B.3 | 08-17 | TODO: twiddle-loader FUN_18014ec20 (Dekker+Cody-Waite), bit-reverse 18003b6c0, транскрипция fft.cpp | | B.4 | 08-17 | burst500: corr 0.995, SNR 19.8 dB, diff −0.065 dB; детектор = уровнезависимый floor | | B.5 | 08-17 | FUN_180535880/536f90 — реальный SSE-код в дампе; .vst3 совпадает с дампом в `.text` | | B.6 | 08-17 | Dispatch-модель исправлена (у каждого стаба своя таблица); полный twin-алгоритм; артефакты `det_*.dis` | | B.7 | 08-18 | band-структура (+0x800 mode/0x804 freq/0x808 sens/0x80c Q), writer 5316e0 (17 case); XML mode=1 → case8 (RBJ bell) | | B.8 | 08-18 | Коррекция middle=−2cos(w0); `mask(f)=|2B/A|(fc,Q,gain)`; freq-path 530850 + warp, level-path 56e3e0 | | B.9 | 08-18 | Level-path замкнут (потребитель 529fe0, аккумулятор 0x5407c8); формула 530d30; warp-ось sigmoid; конструктор 535ae0 (0x24=40000) | | B.10 | 08-18 | Единая модель `C=depth·tilt·D0·(L0/res)^p`; парадокс dual_b1q решён (tilt-наклон); rmse 0.109 dB (31 pt); `model_dual.py` | | B.11 | 08-18 | LUT-кривая извлечена (36 pt на одну кривую); PCHIP 10 узлов rmse 0.0718 dB; колено x≈0.58; `model_lut.py` | | B.12 | 08-18 | Bridge-модель `C=g·LUT(xv)+w·warp^a` (a≈π, аддитивно), rmse 0.236; FFT-conv размаплен; freq-axis offline const; `model_fir.py` | | Ph.5 | 08-18 | `render_parity.py`; dB-parity 36 pt rmse 0.268 dB (рефит 0.203); остаток структурный (0x540658) | | B.13 | 08-19 | res_power `gain=(1−C)·res^rp` (500Hz resid решён); декомп 563440/563ce0/529fe0; константы из дампа | | B.14 | 08-19 | **Артефакт-коррекция** (честная тримнутая метрика; базлайн scalar=0.280); Q-dep rp → mean 0.175; 530d30 повторно NEGATIVE (N=2048) | | B.15 | 08-19 | Диагноз LUT-немонотонности → joint free-knot LUT (8 узлов, dual+al_*) → **dual mean≈0.027 dB**; остаток al_* lv24 dC=+0.10 | ## Риски - **FFT bit-exact**: DSP-STFT FFT найден и транскрибирован; главный нерешённый риск — порядок double-операций в dispatch-ядрах при побайтовом сравнении. - **Хост-зависимость**: точный размер буфера/фрейма (рендеры при разных RENDER_RANGE должны давать одинаковые байты). - **Stereo**: все текущие свипы mono; для bit-exact графа M8 нужны стерео-рендеры (link/balance/ms). - **Честная метрика**: тримнутая длина выхода обязательна (артефакт 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-терм**: эмпирическая формула — аппроксимация runtime-вычисления FUN_180563440; для bit-exact заменить на реальный расчёт. 3. **Стерео-верификация (M8)** — приоритет mono-путь или сразу стерео-граф. 4. **0x540658 window** — ✅ **ЗАКРЫТ 2026-08-19**: live-захват через registry heartbeat (`{u64 count, u64 ptr}` run at psy. 0x29b06c0) во время offline-рендера. Окно = registry[01] (8193 f32, 0.5→1.0, сатурация), freq-axis = registry[0d] (0..23988 Hz, spacing 11.713 ⇒ **внутренний SR=48000**), веса [03][04][05][06] (WA/WB/WC/WD). Сохранено: `handoff/rtwin_freq_44100.npy`, `rtfreqaxis_48000_internal.npy`, `rtwa/rtwb/rtwc/rtwd_*.npy`. Инфра: `rtsnap_fast.py`, `rtsnap2.py` (двухточечный diff подтвердил стабильность таблиц), `findctx.py`. Полное описание: `handoff/NOTES_CAPTURE.md`. vptr/44100-marker объект-скан (rtobj) — тупик (производная vtable не адресуется статически; регистр-бикон — рабочий путь). ## Где лежат детали - `handoff/SESSION_HANDOFF.md` — инвентарь декомпа (§0), трансляция/ключевые адреса (§2), Phase-5 план (§6). - `handoff/NOTES_TWIN.md` — twin-резонатор, caller, grid/oversample. - `handoff/NOTES_LEVEL.md` — level-path, LUT-нога, res_power протокол, спектральные веса. - `handoff/nls_dasm/` — 120+ дизассемблей (f_563440, f_563ce0, f529fe0, twin, iface, fft). - `notes_giant_fft.md` — FFT-планировщики/ядра/twiddle/Cody-Waite. - `git log` — полная журнальная хронология с номерами коммитов.