diff --git a/README.md b/README.md index 15f0456..0a198b5 100644 --- a/README.md +++ b/README.md @@ -3,33 +3,33 @@ Обратный инжиниринг DSP-ядра **oeksound soothe2** (VST3, Windows x64) → проверяемая численная модель и реконструкция на C++. -Цель — разобраться, как именно плагин считает подавление резонансов, и воспроизвести -его поведение (в идеале бит-точно, но пока — с точностью ~0.1 dB на формах полос). +Цель — понять, как именно плагин считает подавление резонансов, и воспроизвести +его поведение (в идеале бит-точно, но пока — с точностью ~0.03–0.18 dB на формах +полос и уровнях). --- -## Статус (B.10, август 2026) +## Статус (B.15, август 2026) -**Детектор расшифрован структурно и проверен численно на двух независимых наборах -измерений (31 точка, совместный фит rmse = 0.109 dB).** +**Полный STFT-рендер** пайплайна работает в `framed_render.py` (N=2048, hop=512, +sqrt-Hann, per-bin twin-envelope tatt=11ms/trel=80ms). Модель: +`C(f_k)=g·LUT(log10(A_k/res_k)) + w·warp(f_k)^a`, `gain=(1−C)·res^(rp0·Q^drp)`. -- **Freq-path (форма полосы)** — близнец `FUN_180535880`/`FUN_180536f90` (резонатор - 2-го порядка). Коэффициенты case8 (mode=1 исполняется как case8!): - `w0=2π·fc/fs; p=(sin(w0)·0.5)/Q; A=[p·gain+1, −2cos(w0), 1−p·gain]; - B=[p/gain+1, −2cos(w0), 1−p/gain]; res(f)=|2·B/A|(f)`. -- **Level-path (глубина)** — per-band маска `= depth·tilt(f)·D0·(L0/res(f))^p`, - где `tilt(f)=1−w(f)` — per-bin спектральный вес из `FUN_180530d30` - (`w[i] = log10(1/exp(...·exp(2000/f_bin))...)`), `L0` — уровень входа, `p≈0.085`. -- **Ключевые факты**: - - `gain ≈ 10^(sens/20)` (sens=12 → gain≈4.1); - - частотная селективность — НЕ notch самой маски, а резонанс в знаменателе: - врез глубже там, где `L0/res` больше (на центре полосы); - - «парадокс dual_b1q» (red@2000 > red@500 при низком Q) решён: это частотный - наклон `tilt(f)` (растёт с частотой), а не ширина резонанса; - - warp-ось `0x540748` = сигмоида `8.3 − 7/(1+exp((x·20000−120)·(−0.01)))`, - а НЕ `f/(f+K)`, как ошибочно предполагалось раньше. +- **Честная (trimmed) метрика** — длина выхода = длина входа (не-тримнутые замеры + давали ложный сдвиг ~0.2 dB, исправлено в B.14). Базлайн для scalar-rp = 0.280 dB. +- **Q-dependent rp** `rp(Q)=rp0·Q^drp` (rp0=0.0275, drp=0.2159) → **mean=0.175 dB**: + q0.1 и q10 почти идеальны, боттлнек q1@2000 (−0.71 dB). +- **JOINT free-knot LUT** (8 узлов Pchip, G/W/A/rp0/drp) → **dual mean≈0.027 dB**, + q1@2000 закрыт до 0.000; остаток al_* lv24 dC=+0.10 (зона xv<−0.3). +- **Декомп DSP-ядра в основном закрыт**: twin-резонатор, генератор case8, level-weight + (0x530d30 — численно НЕ create tilt, подтверждено при N=2048), LUT-кривая FUN_180563440 + (linear/power-law по флагу), IIR-трекеры FUN_180563ce0, FFT-conv 0x535a70 — + всё размаплено, `.dis` в `handoff/nls_dasm/`. -Подробная история, гипотезы и все промежуточные находки — в [`roadmap.md`](roadmap.md). +```bash +# Текущий канон (Q-dep rp, trimmed): +python3 framed_render.py dual +``` --- @@ -38,89 +38,74 @@ ``` re-tools/ ├── README.md ← вы здесь -├── roadmap.md ← журнал всех результатов/гипотез (B.1…B.10) -├── model_dual.py ← КАНОНИЧЕСКАЯ верифицированная модель (B.10) +├── roadmap.md ← журнал всех результатов/гипотез (B.1…B.15) +├── framed_render.py ← КАНОНИЧЕСКИЙ frame-рендер (STFT+LUT+warp+res_power) +├── render_parity.py ← dB-parity харнесс (Goertzel steady-state замер) +├── model_lut.py, model_dual.py ← исторические модели B.10/B.11 (collapse-фиты) +├── model_fir.py ← bridge-модель B.12 (C=g·LUT+w·warp^a) +│ +├── handoff/ ← перекидка между сессиями +│ ├── SESSION_HANDOFF.md ← инвентарь декомпа, трансляция, Phase-5 план +│ ├── NOTES_TWIN.md ← twin-резонатор + caller + grid +│ ├── NOTES_LEVEL.md ← level-path, LUT-нога, res_power протокол +│ └── nls_dasm/ ← 120+ дизассемблей (f_563440, f_563ce0, f529fe0, twin…) │ ├── dsp/ ← реконструкция DSP-пайплайна на C++17 -│ ├── rotor_kernel.hpp ← бит-точное ядро ротора (близнец детектора) +│ ├── twin{.hpp,.cpp} ← бит-точный twin-резонатор (FUN_180535880) │ ├── spectral.cpp/.hpp ← WOLA/STFT-обработчик │ ├── detect.cpp/.hpp ← детектор резонансов │ ├── filter.cpp/.hpp ← биквад-фильтры │ ├── fft*.cpp, twiddle_*.cpp/hpp, phase_table.* ← FFT-планы/твилдлы │ ├── ms.hpp ← encode/decode mid/side -│ ├── cody_waite.hpp ← быстрый exp/log (Cody–Waite) +│ ├── cody_waite.hpp ← быстрый sin/cos (FUN_1801de760/1e3f20) │ ├── soothe_constants.hpp ← константы из дампа │ ├── harness.cpp ← CLI: input.wav → output.wav (24-bit) │ └── CMakeLists.txt │ ├── *.java ← Ghidra-скрипты (analyzeHeadless, пост-скрипты) -│ ├── ListFuns / DumpFuns / DumpList ← списки функций/декомпиляция -│ ├── DumpVtables / DumpDsp / DumpCandidates -│ ├── ImportRtti, SearchRefs, Diag ← RTTI/перекрёстные ссылки -│ └── ... -│ ├── ghidra-proj/ ← Ghidra-проект (soothe2.gpr/.rep), вне git ├── soothe_mem.bin ← дамп памяти плагина (frida), вне git -├── regions/, regions_* ← регионы памяти из дампа -├── phase_table_1024.f64/.json← таблица фаз (FFT) +├── rwin_{A0,A1,B0,C0}.npy, r_freqaxis.npy ← живые таблицы (48k) из runtime-снимков │ -├── Измерение и фиты полосы: -│ ├── measure.py ← анализ WAV: глубина вреза, attack/release тау -│ ├── probe.py ← спектральный анализ тонов -│ ├── bandshape.py ← эмпирическая форма полосы из рендеров -│ ├── fit_bandshape.py ← фит формы полосы -│ ├── fit_curves.py ← фит кривых (глубина/частота) -│ ├── fit_maskaxis.py ← brute-force подбор формулы маски (B.7/B.8) -│ ├── model_dual.py ← ВЕРИФИКАЦИЯ модели (dual_b1q + t1kq) -│ └── notch.py ← расчёт/проверка notch -│ -│ ── Поведенческие симуляторы: -│ ├── sim.py, sim_v5.py ← STFT-детектор (эволюция гипотез о архитектуре) -│ └── verify_sim.py -│ -│ ── Инструменты REAPER-рендеров: -│ ├── sweep.py ← генерит .rpp с изменением параметров плагина -│ ├── run_sweep.py, tt_sweep.py, run_verif*.py -│ ├── patchparam.py ← патчит PARAM в base64-блоке состояния -│ └── addfx.lua ← добавление soothe2 в проект -│ -│ ── Живая трассировка (yabridge-host + Frida): -│ ├── dump_soothe.py ← дамп всей памяти плагина → soothe_mem.bin -│ ├── rtall.py / rtscan.py / rtsig.py / rttbl.py / rtver.py -│ ├── rtone.py / rtsig.py / rtwin.py / rtdeep*.py / rtdump*.py -│ └── probe.py, procdump.py, ghidra_diag.py, mkbase.py -│ -├── *.txt / *.log ← декомпиляции, лямбли-дампы, xrefs (архив) -├── summary.md, notes_giant_fft.md -└── curve_fits.npz, depthcurve.npy, consts*.txt +├── Измерение и фиты полосы: measure.py, probe.py, bandshape.py, fit_*.py, +│ model_lut.py, notch.py +├── Поведенческие симуляторы: sim.py, sim_v5.py, verify_sim.py +├── Инструменты REAPER-рендеров: sweep.py, run_sweep.py, tt_sweep.py, +│ patchparam.py, addfx.lua +├── Живая трассировка (yabridge-host + Frida): dump_soothe.py, rtall/rtscan/ +│ rtsig/rttbl/rtver/rtone/rtwin/rtdeep*.py, probe.py, procdump.py +└── summary.md, notes_giant_fft.md ``` ### Тестовый корпус `/home/m/soothe-bt/` (вне git) -~600 рендеров REAPER (`.rpp` + `.wav`), сгенерированы скриптами выше. Ключевые наборы: +~600 рендеров REAPER (`.rpp` + `.wav`). Ключевые наборы: | Префикс | Что измеряет | |---------|-------------| -| `burst500_b1` | основной эталон: burst 500 Гц, band1 fc=500 mode=1 sens=12 depth=0.864 | -| `dual_b1q_*` | 2 тона (500+2000), q полосы 0.1…10 → **парадокс red2000>red500** | -| `t1kq_only1_*`| 1 тон 1 кГц, fc-скан → форма полосы (rmse 0.047) | -| `t1k_b1f_*`, `t1kq_b1f_*` | fc-сканы, другие q/сенс | -| `tone1k`, `tone1kq`, `dual`, `n5_b1q_*`, `b2*`, `al_*` | уровни/частоты/сенсы | - -Входы: `dual.wav` (500+2000 Гц, −7.1 dBFS), `tone1k.wav` (0 dBFS), `tone1kq.wav` (−18 dBFS). +| `dual_b1q_*` | 2 тона (500+2000), q 0.1…10 → **парадокс red2000>red500** (решён: аддитивный warp) | +| `al_*` | уровневой свип (fc=1000) → прямое измерение LUT-кривой | +| `t1kq_*`, `t1k_*` | 1 тон, fc-скан, уровни −18/0 dBFS → форма полосы | +| `burst500_b1` | основной эталон: burst 500 Гц, band1 fc=500 mode=1 sens=12 | +| `tone1k`, `tone1kq`, `dual` | входные тоны (0 / −18 / −7.1 dBFS) | --- -## Модель (B.10) в двух строках +## Модель в трёх строках ```python -red(f) = -20*log10(1 - C(f)) -C(f) = depth * tilt(f) * D0 * (L0 / res(f))**p -# res(f) = |2B/A|(f; fc, Q, gain) — case8, tilt(f) = 1 - w(f) — веса 0x530d30 -# p ≈ 0.085, gain ≈ 10**(sens/20), tilt ratio(2000/500) ≈ 1.27 +# per-frame, per-bin (framed_render.py — канон) +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) ``` -Проверить: `python3 model_dual.py` (31 точка, rmse 0.109 dB). +- LUT — Pchip, узлы из joint-фита (dual+al_*) **или** свободные 8 узлов (B.15 → mean 0.027); +- warp — эмпирический `0.87·7.942·x/(7.942+x)`, x=f/2000 (аппроксимация runtime-вычисления FUN_180563440); +- rp(Q)=0.0275·Q^0.2159 (Q-dep rp, B.14); +- B.15: G/W/A/rp0/drp = 0.9752/0.3394/1.0222/0.0254/0.2231; LUT KX=[−0.8..1.0]. + +Проверить: `python3 framed_render.py dual` (mean 0.175 для Q-dep rp). --- @@ -132,16 +117,17 @@ C(f) = depth * tilt(f) * D0 * (L0 / res(f))**p -noanalysis -postScript .java` (image_base=0, блок `ram`=[0x180000000,…], адреса = RVA; смещение в файле = VA−0x180000000). 3. **Рендеры**: `sweep.py` генерит `.rpp`, `reaper -renderproject` выдаёт `.wav`. -4. **Анализ**: `measure.py fx.wav dry.wav --fc 500` → глубина/тау; - `bandshape.py` → форма полосы; `model_dual.py` → верификация модели. +4. **Анализ**: `render_parity.py` / `framed_render.py` сравнивают модель с реальными + рендерами (steady-state tone-замер, тримнутая метрика). --- ## Чего не хватает / следующие шаги -- Бит-точная сверка рендера: собрать `dsp/harness.cpp`, прогнать `burst500_b1.wav` - и сверить байт-в-байт (сейчас модель — численная, а не побайтовая). -- Семантика IAT-хелперов `0x181a14xxx` (exp/log/pow) и AVX-ядер (`FUN_18001a5a0` - и др.) — целевые адреса вне дампа. -- Точная функция LUT `level→mask` (кривая `param_1+0x188`) вместо аппроксимации `^p`. -- Полный конвейер WOLA/oversample (offline 3×) и sidechain/стерео-путь. +- **Структурная LUT FUN_180563440** вместо Pchip: закрыть остаток al_* lv24 (dC≈+0.10, + зона xv<−0.3) и q1@500 (+0.06). +- **Бит-точная сверка**: собрать `dsp/harness.cpp`, прогнать `burst500_b1.wav` + и сверить байт-в-байт (сейчас модель — численная, rmse 0.03–0.18 dB). +- C++ порт res_power + Q-dep rp + свободных LUT-узлов (тривиально: `gain *= pow(res, rp0·Q^drp)`). +- Семантика IAT-хелперов `0x181a14xxx` (exp/log/pow) и AVX-ядер — целевые адреса вне дампа. +- Полный конвейер WOLA/oversample (offline 3×) и sidechain/стерео-путь. \ No newline at end of file diff --git a/roadmap.md b/roadmap.md index ce62b91..7b8c5aa 100644 --- a/roadmap.md +++ b/roadmap.md @@ -9,6 +9,25 @@ (WAV16 → рендер → WAV24), `dsp_notes.md` (карта функция↔смысл, константы, LUT), `verify_bit_exact.py` (прогон всех свипов, побайтовое сравнение). +## Текущий статус (2026-08-19) + +**Статический декомп DSP-ядра — В ОСНОВНОМ ЗАКРЫТ** (A.2/A.3/FFT/twin/generator/level-path/LUT/IIR декодированы, +`.dis` в `handoff/nls_dasm/`). **Численная модель — frame-рендер `framed_render.py`** (STFT N=2048, hop=512, +sqrt-Hann, twin-env tatt=11ms/trel=80ms, LUT+additive-warp+res_power). **Честная тримнутая метрика** +(длина выхода = длина входа): Q-dep rp → **mean=0.175 dB** (q0.1/q10 идеальны, боттлнек q1@2000=−0.71); +joint free-knot LUT + dual+al_* фит → **dual mean≈0.027 dB** (B.15). Цель bit-exact (Phase C) — в работе; +остаток структурный (форма LUT в зоне xv<−0.3, warp-терм). + +```bash +# Простейший запуск текущего канона (Q-dep rp): +cd /home/m/re-tools && python3 framed_render.py dual +# q0.1: 500 +0.00 / 2000 +0.00 · q1: −0.26/−0.71 · q10: +0.09/+0.00 · mean 0.175 +``` + +**ВАЖНО про метрику**: ранние записи "mean=0.160/0.144" считались на выходе НЕтримнутой длины +(`nfr*HOP+N`) и оказались артефактом; честный сравнительный базлайн для scalar `rp=0.0169` = **0.280** +(не 0.160). Все новые числа ниже — на ТРИМНУТОЙ метрике. + ## Задел (уже есть) - Ghidra-проект `ghidra-proj/soothe2.rep` + скрипты `Dump*.java`/`ImportRtti*.java`/`Diag.java`. - RTTI иерархия DSP: `SpectralProcessor`, `Soothe2ModuleBase`, @@ -23,6 +42,13 @@ - Рендер-свипы в `/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 протокол). ## Контракт из manual (`soothe2_ManualFAQ.pdf`, v1.0.0) @@ -71,7 +97,7 @@ - A.5. Найти oversample/resolution-путь (M6/M7): интерполяция сетки, период обновления. - A.6. Сопоставить топологию с диаграммой (0.1) и stereo/link/balance (M8). -### Статус фазы A (~40%) +### Статус фазы 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). @@ -111,16 +137,21 @@ - C.3. Итеративный цикл diff → локализация блока → фикс → повтор. ## Риски -- **FFT bit-exact**: свой планировщик есть только у Vorbis-декодера; DSP-STFT-FFT ещё не найден (A.3) — - если свой radix-2 — воспроизводим напрямую; главный нерешённый риск фазы A. +- **FFT bit-exact**: DSP-STFT FFT найден (планировщики-гиганты + stage-ядра, см. recap ниже); + воспроизводим напрямую. Главный нерешённый риск bit-exact — порядок double-операций в dispatch-ядрах. - **Хост-зависимость**: уточнить точный размер буфера/фрейма (рендеры при разных RENDER_RANGE должны давать одинаковые байты). - **Stereo**: все текущие свипы mono; для бит-exact графа M8 нужны стерео-рендеры (link/balance/ms). +- **Честная метрика**: тримнутая длина выхода обязательна; НЕ-тримнутые замеры дают ложный сдвиг ~0.2 dB + (артефакт B.14). Все fit-цели перепроверять на `render_trim`/`framed_render.synthe`. ## Открытые вопросы -1. Где реальный DSP-STFT FFT (не Vorbis/не внешний lib)? Искать большие функции с butterfly/стадиями - в 18052*/18053* или через трассировку вызовов от `processBlock` спектрального процессора. -2. Окна/oversample детали — из декомпиляции (A.6) или требуется доп. замеры. -3. Стерео-верификация (M8) — приоритет mono-путь или сразу стерео-граф. +1. **Форма LUT в зоне xv<−0.3**: joint-fit свободных узлов даёт dual≤0.07, но al_* lv24 dC=+0.10 — + нужна структурная кривая FUN_180563440 (linear/power-law по флагу полосы) вместо Pchip-эмпирики. +2. **warp-терм**: эмпирический `0.87·7.942·x/(7.942+x)` — аппроксимация runtime-вычисления + (не таблица); для bit-exact заменить на реальный расчёт FUN_180563440. +3. **Стерео-верификация (M8)** — приоритет mono-путь или сразу стерео-граф. +4. **0x540658 window** — статически невидим; живые копии в `rwin_*.npy` (48k) требуют + warp-нормализации под 44.1k рендеры. ## Phase A progress update (recap) @@ -456,3 +487,63 @@ - **Файл**: `/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.