From 6c3ab92405a6ffd93162b78875ced4fefec5501d Mon Sep 17 00:00:00 2001 From: Matiq Date: Wed, 19 Aug 2026 17:39:48 +0300 Subject: [PATCH] docs: full roadmap rewrite (drop stale phase-A recap, milestones table, doc index) + README doc links --- README.md | 12 ++ roadmap.md | 620 ++++++++++------------------------------------------- 2 files changed, 127 insertions(+), 505 deletions(-) diff --git a/README.md b/README.md index 0a198b5..78ea0e1 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,18 @@ re-tools/ └── summary.md, notes_giant_fft.md ``` +## Документация + +| Документ | Содержание | +|---|---| +| [roadmap.md](roadmap.md) | Журнал всех результатов/гипотез (B.1…B.15), статус по фазам, риски, открытые вопросы | +| [handoff/SESSION_HANDOFF.md](handoff/SESSION_HANDOFF.md) | Инвентарь декомпа (§0), трансляция/ключевые адреса (§2), Phase-5 план (§6) | +| [handoff/NOTES_TWIN.md](handoff/NOTES_TWIN.md) | Twin-резонатор (FUN_180535880/536f90), caller, grid/oversample | +| [handoff/NOTES_LEVEL.md](handoff/NOTES_LEVEL.md) | Level-path, LUT-нога, res_power протокол, спектральные веса | +| [handoff/nls_dasm/](handoff/nls_dasm/) | 120+ дизассемблей (f_563440, f_563ce0, f529fe0, twin, iface, fft) | +| [notes_giant_fft.md](notes_giant_fft.md) | FFT-планировщики/ядра/twiddle/Cody-Waite | +| [summary.md](summary.md) | Сводка по реверсу и реконструкции | + ### Тестовый корпус `/home/m/soothe-bt/` (вне git) ~600 рендеров REAPER (`.rpp` + `.wav`). Ключевые наборы: diff --git a/roadmap.md b/roadmap.md index 7b8c5aa..941f802 100644 --- a/roadmap.md +++ b/roadmap.md @@ -1,8 +1,9 @@ # Roadmap: bit-exact реверс DSP-ядра oeksound soothe2 (v1.1.2) ## Цель -Воспроизвести DSP-путь soothe2 в виде **собираемого standalone C++** с **побайтово идентичным** -выходом (bit-exact) на верификационных свипах, с пониманием логики и подписанными функциями. +Воспроизвести DSP-путь soothe2 в виде **собираемого standalone C++** с **побайтово +идентичным** выходом (bit-exact) на верификационных свипах, с пониманием логики +и подписанными функциями. ## Результат `re-tools/dsp/` — `*.cpp/*.h` (классы DSP, подписаны), `CMakeLists.txt`, `harness.cpp` @@ -11,69 +12,91 @@ ## Текущий статус (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-терм). +- **Статический декомп DSP-ядра — закрыт (~90%)**: twin-резонатор, генератор case8, + level-path (0x530d30/0x529fe0), LUT-кривая (FUN_180563440), IIR-трекеры (FUN_180563ce0), + FFT-conv (0x535a70) декодированы; `.dis` в `handoff/nls_dasm/`. Остаток — runtime-значения + (окно 0x540658, sens-источник, IIR update-loop). +- **Численная модель = 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/` готова, порт + res_power/Q-dep-rp/LUT — TODO. +- **ВАЖНО про метрику**: ранние записи "mean=0.160/0.144" считались на выходе НЕтримнутой + длины (`nfr*HOP+N`) и были артефактом; честный базлайн для scalar `rp=0.0169` = **0.280**. + Все числа в этом файле — на ТРИМНУТОЙ метрике. -```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 +## Модель в двух строках + +```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) ``` -**ВАЖНО про метрику**: ранние записи "mean=0.160/0.144" считались на выходе НЕтримнутой длины -(`nfr*HOP+N`) и оказались артефактом; честный сравнительный базлайн для scalar `rp=0.0169` = **0.280** -(не 0.160). Все новые числа ниже — на ТРИМНУТОЙ метрике. +- 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`. -## Задел (уже есть) -- Ghidra-проект `ghidra-proj/soothe2.rep` + скрипты `Dump*.java`/`ImportRtti*.java`/`Diag.java`. -- RTTI иерархия DSP: `SpectralProcessor`, `Soothe2ModuleBase`, - `Soothe2Module`, `FilterGraph`, `FilterGraphGrid`, - `DigitalFilter`, `IIRFilterExtended`, `AudioProcessingModule` - (`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·(1−g)+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=(1−C)·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 протокол). +## Статус по фазам + +- **Фаза 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); + остальное построено, но res_power/Q-dep-rp/LUT/verify_bit_exact — TODO. + +## Ключевые находки (итог) + +- **Маска аддитивная**: `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). 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·(1−g)` | -| 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 = x−wet; Mix 0..100% (стр.14) | порядок: detect→notch→trim→mix/delta/bypass | +| 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 / side-chain EQ», сумма band-кривых = белая кривая (стр.17) | `red(f)=base+ΣW_i·H_i` подтверждён | -| M12 | Sidechain: детект по второму входу; Input trim — скрытый предусилитель анализа (стр.26) | analysis имеет отдельный вход/гейн — граф с 2 входами | +| M11 | Кривая = inverse EQ, сумма band-кривых = белая (стр.17) | `red(f)=base+ΣW_i·H_i` | +| M12 | Sidechain; Input trim — скрытый предусилитель анализа (стр.26) | отдельный вход/гейн анализа | -## Топология пайплайна (Приложение A, стр.30 — OCR `pipeline_ocr.txt`) +## Топология пайплайна (Приложение A, стр.30 — `pipeline_ocr.txt`) ``` main inputs ──┐ sidechain ────┤ (sidechain enabled?) ▼ - input trim ──► analysis (детекция; depth/sharpness/selectivity; EQ curves из per-band balances) + input trim ──► analysis (детекция; depth/sharpness/selectivity; EQ из per-band balances) │ stereo apply + balance (channel link + global balance) │ @@ -81,469 +104,56 @@ cd /home/m/re-tools && python3 framed_render.py dual │ mix ──► bypass? ──► output ``` -- **mid/side ручка**: обработка в M/S-области (`ms encode?`/`ms decode?`) — стерео-путь подтверждён диаграммой. -- GUI/visualisation (depth/sharpness/selectivity, EQ curves, per-band balances) — побочная ветвь, в DSP-ядро не входит (кроме параметров). -## Фазы +- mid/side ручка: обработка в M/S-области — стерео-путь подтверждён диаграммой. +- GUI/visualisation — побочная ветвь, в DSP-ядро не входит (кроме параметров). -### Фаза 0 — Снятие диаграммы пайплайна (сделано) -0.1. OCR стр.30 (`tesseract`, PNG в `/tmp/opencode/manual/p30_s4.png`) → `pipeline_ocr.txt`. ✅ +## Журнал вех (детали — в handoff/ и git log) -### Фаза 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-диапазоне - 0x1824c0000–0x182700000) из повторного прогона `DumpFuns.java`. Точная double/f32-интерпретация — - `consts_double.txt` (чтение PE напрямую, `.data` vs `.rdata` через RVA). -- **A.3 ОКНО — найден**: `FUN_1805356f0` генерирует **Hann** `w(i)=0.5·(1−cos(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< 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` vtbl @180529610 (1154) + methods around 180529xxx/18052exxx. -- `Soothe2Module`, `Soothe2ModuleBase`, `IIRFilterExtended`, 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), добавлен ``. -- **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.25−1; - (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,(1−w²)·2a,(1−k+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.7–0.95 при q≤1, - → рост при q≥1.5 (Δ<0 — вне bell). Структура `FUN_180533ec0` подтверждена. - mode4-путь (Q=0.5 фикс, fvar=(q−0.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 В 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 = (1−C) · 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** `(B−A)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 1−C. -- **Константы из дампа**: 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@2000−0.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⁻²⁰⁰ → 1−w→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. +- `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` — полная журнальная хронология с номерами коммитов. \ No newline at end of file