Files
soothe2-re/roadmap.md
T

40 KiB
Raw Blame History

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_18052e130N = 2^floor(log2(sr/44100))·N0 (44100 @0x1824c45ec, 2.0 @41e8). Нормализация окна в FUN_18052df60: сумма по окну → scale=1/(mean/N), потом /= (fsize @+0x1ac).
  • A.3 FFT — НАЙДЕН (свой): Vorbis-планировщик FUN_181384180 — не тот (это декодер). Настоящий DSP-FFT: FUN_180008140 = init FFT-spec (N=1<<log2, масштаб DAT_181c5cd10(=1.0)/N для inverse, ветки float/double/complex), строит twiddle/bitrev через FUN_18003a2a0 (radix-2/3/5 для малых N), FUN_18003f1a0+FUN_18003f600 и FUN_18003b6c0 (N≥2^18: бит-реверсовая пермутация с XOR-маской знака DAT_181c5e4e0). Внешних lib (kissfft/fftw/MKL) нет. Xrefs на init только из диспетчерской таблицы 182615ef0 (+ .pdata) — вызов косвенный.
  • Диспетчерские таблицы (PACE/ModuleDecryptor): все DSP-примитивы вызываются через thunk_FUN_181ba94b0@0x180001850 → слот из таблиц PTR_FUN_1826159c8/5a88/6408... по глобальному индексу DAT_1826159a0. 36 таблиц, 245 уникальных функций-целей (dispatch_targets.txt). Примеры ядер: FUN_1800025e0 = double-вектор add (развёртка по 16), FUN_18003fa20 = комплексное умножение a[i]·b[i] (фрейм-пар, 425б). Для bit-exact нужен порядок double-операций.
  • Константы детектора: 10^((dB)/20) через expf((30·X90)·0.115129255), 0.115129255=1/(20·log10e), 6.907755=ln1000, 0.707/0.51/0.54/0.001, маппинг резонанса (x0.707)·0.5+1 в FUN_1805316e0.

Фаза B — Реконструкция C++ (bit-exact)

  • B.1. Классы 1:1: Soothe2FilterGraph::processBlock, FilterGraphGrid, DigitalFilter, SpectralProcessor (STFT), AudioProcessingModule, IIRFilterExtended, env-модуль с частотным масштабом (M5), интерполятор oversample (M6), периодичность resolution (M7).
  • B.2. Точный порядок FP-операций float32, без перестановок; буферный фрейминг независимо от блока хоста.
  • B.3. Имена по смыслу + подписи, сверяемые с моделью sim_v5 (base_red, W, Qeff, sat, Q_notch).
  • B.4. Модули по manual: mode (M1), input trim/sidechain (M12), trim/delta/mix/bypass-порядок (M9), stereo/link/balance + ms encode/decode (M8).

Фаза C — Сборка и bit-exact-верификация

  • C.1. libsoothe2_dsp.so + CLI-харнесс (WAV16→рендер→WAV24, как Reaper).
  • C.2. Критерий: каждый свип (trk_*, wf_*, qmap, sens, lvl, neut, al, psh, dual, pair, chirp, burst, tj) — побайтовая идентичность int24 PCM с рендерами.
  • C.3. Итеративный цикл diff → локализация блока → фикс → повтор.

Риски

  • FFT bit-exact: свой планировщик есть только у Vorbis-декодера; DSP-STFT-FFT ещё не найден (A.3) — если свой radix-2 — воспроизводим напрямую; главный нерешённый риск фазы A.
  • Хост-зависимость: уточнить точный размер буфера/фрейма (рендеры при разных RENDER_RANGE должны давать одинаковые байты).
  • Stereo: все текущие свипы mono; для бит-exact графа M8 нужны стерео-рендеры (link/balance/ms).

Открытые вопросы

  1. Где реальный DSP-STFT FFT (не Vorbis/не внешний lib)? Искать большие функции с butterfly/стадиями в 18052*/18053* или через трассировку вызовов от processBlock спектрального процессора.
  2. Окна/oversample детали — из декомпиляции (A.6) или требуется доп. замеры.
  3. Стерео-верификация (M8) — приоритет mono-путь или сразу стерео-граф.

Phase A progress update (recap)

FFT transform (found)

  • Recursive FFT planners ("giants"): 7 monolith funcs (~14.4K each) = type variants: 18001f260, 1800246e0, 18002f980, 180029ca0, 180035980, 18003b900, 18004ff80 Recurse on log2 - factor with param_5+1 depth; build stage plan in param_1+0x78 array.
  • Factor tables (int, index=log2): DAT_181c5e0fc (small=4/8/9/13), DAT_181c5e15c, DAT_181c5e3fc (nonzero => use giant path in FUN_18003b6c0).
  • FUN_18003b6c0 (bit-reverse+plan, N big): if DAT_181c5e3fc[log2]!=0 call giant FUN_18003b900, else small plan via DAT_181c5e4e0 XOR mask permute.
  • Base small FFT: FUN_18002e360 (radix2/3), FUN_180023860 (float variant).
  • Twiddle: copied from static base DAT_182616800 into scratch by FUN_180039b00.
  • Spec init FUN_180008140 (from dispatch table 182615ef0): builds plan; dispatch-only callers.
  • Giants/planners = integer address gen (no float math) => executors are separate (dispatch kernels).

Spectral layer located (named C++ classes via RTTI):

  • SpectralProcessor<float,7,1> vtbl @180529610 (1154) + methods around 180529xxx/18052exxx.
  • Soothe2Module<float,1>, Soothe2ModuleBase, IIRFilterExtended<float,1>, ModuleDecryptor.
  • range 0x180500000-0x180530000 listed (1051 fns) -> funs_range.txt overwritten.

Block processor chain (audio->out):

  • FUN_18052e260(param_1, n) : reads det? calls FUN_180536300(param_1+0x3d8, in, out, det, n)
  • FUN_180536300 (475): normalize DAT_1824c4248/(freq), FUN_18052da00/db50/d990/dbc0 (smoothing), FUN_180535880, FUN_180536f90 (249, detector update via dispatch).
  • Param setup fns: FUN_18052e9b0 (3167), FUN_180529fe0 (2051) — float math on params (DAT_18262b700 block-sizes etc.).

Audio chain (xrefs confirmed)

  • Soothe2Module::processBlock (0x18052b?? call at 18052b99c) -> FUN_18052e260 (reads, calls block proc)
  • -> FUN_180536300(param_1+0x3d8, in, out, det, n) (also called from 0x18058e3df)
  • which calls: FUN_18052da00(normalize), FUN_18052db50(fill/0), FUN_180535880, FUN_180536f90, FUN_18052d990, FUN_18052dbc0 (smoothed combine), FUN_18052db50.
  • All small DSP ops = PACE stubs __std_type_info_compare + thunk_FUN_181ba94b0 (dispatch).
  • vftable SpectralProcessor = PTR_FUN_182615990; methods listed in rtti_dsp.json (many are ModuleDecryptor/PACE slots, NOT dsp).
  • Param/curve generators (exp/log/pow/tan based, DAT_1824c4xxx): FUN_1805316e0 (6238), FUN_180529fe0 (2051), FUN_18052e9b0 (3167).
  • Dispatch index global DAT_1826159a0 written only in PACE wrappers 18000101b/180001046.
  • big spectral-area functions are mostly param/memory/PACE; the real dsp sticks are dispatch kernels (245 targets).

DSP kernel map (xref cross-check)

  • Complex multiply FUN_18003fa20(double*, cplx[][16], n) — called ONLY from FUN_180008440 (init area, twiddle build) — NOT a dispatch slot.
  • Double mul FUN_180008500 — dispatch slot @182615f20; called from FUN_18000c5e0 (double FFT stage kernel, 507B): loop over 0x40-chunks: thunk_FUN_1801560a0(param_2,re,im,0x40) (load twiddle), FUN_180008500(param_1,re,n), FUN_180008500(param_1,im,n), FUN_180044700(im,re,param_3,n).
  • Double add FUN_1800025e0 slot @1826159e8; float max FUN_1800104e0 slot @1826164a0; 180010860 = vmax/vmin with const.
  • Decompiler entry-point trick: getFunctionAt misses overlapping/obfuscated entries => use getFunctionContaining (DumpList updated).
  • OBtained: full FFT-plan model, factor tables, per-type kernels, block->detector->mask flow.
  • Next sub-goal: trace the FFT-stage kernel call order used by detector (which plan fields drive per-stage twiddle/mul) => then C++ re-implementation is a direct transcription of that order.

Transform runtime (tracing complete -> next: reimpl)

  • Plan fields consumed via plan->thunk indirection: thunk_FUN_1801560a0 -> PTR_FUN_1826178a0; FUN_18014ec20 thunk -> PTR_FUN_182617828/850/878 (3 twiddle-loaders).
  • Stage kernels:
    • FUN_18000bfc0 (complex stage, slot @182616208): loop 0x40: load-twiddle(re,im) ; FUN_180008440(cplx-mul, slot @182615f08) twice (re,im) ; FUN_1800437c0(accum). out+=0x200, tw+=0x200, acc+=0x400.
    • FUN_18000c5e0 (double stage, slot @182616220): 0x40-chunks: load-twiddle(re,im); FUN_180008500(double mul, slot @182615f20) x2; FUN_180044700(accum).
  • Dispatch-slot readers (drivers): slot cplx-mul @182615f08 read at 180001c47; stage @182616208 read at 180001e87 => runtime dispatch executors live in 0x180001xxx (thunk chain).
  • DSP runtime model := plan + per-stage kernel calls (all ops = dispatched vector kernels), all through thunk_FUN_181ba94b0 from PACE-protected callers.
  • => Reimplementation path: transcribe stage-loop math (kernel shapes known), replicate plan generation, drive per-stage calls in same order; validate losses to within 0.001 dB on Reaper renders.

Twiddle-builder FUN_180039b00 (29-fun dump, 2026-08-17) — TWO-PATH (key finding)

  • arg1 = log2(N), arg2 = dest buf. Returns buf + (N/4+1)*8, align-down to 0x40.
  • Path N <= 1024 (log2 < 0xb): copies sin VALUES directly from static DAT_182616800 (1024 doubles = sin(k·2π/1024)), stride = 2^(10-log2N); writes buf[k] = sin(k·2π/N); terminator 1.0 at index N/2. NO sin calls — values are precomputed in static table.
  • Path N > 1024: writes ANGLE RAMP buf[k]=k·(2π/N) (two SIMD segments: forward 0..N/8, reversed N/8..0 via vpsubd), then converts with thunk_FUN_1801de760 (sin) and thunk_FUN_1801e3f20 (cos).
  • => loader FUN_18014ec20 (cplx-stage twiddle, 8255B) READS 16B-per-iter (2 doubles) and does Dekker-split math (>>0xc, <<0x28, XOR masks DAT_181c91xxx, Pi-reduction with 1/π, magic 2^52*1.5) => it RECOMPUTES sin/cos from the angle table; it is NOT a plain copy. 3 twiddle-loader slots 182617828/850/878 ALL currently point to 0x18014ec20 (same fn).
  • CONSEQUENCE: for 44100 => N=N0; if N0<=1024 the twiddle = static sin table (reproducible directly); if N0>1024 must replicate Cody-Waite sin/cos of FUN_18014ec20 bit-exactly. N0 (+0x19c) still unfound statically.

N0 RESOLVED - LIVE DUMP @44100 (2026-08-17) — KEY

  • Soothe2 render_short.rpp (44100 Hz) + /proc/pid/mem scan of yabridge-host heap (out file growth confirmed processing; targeted field-signature scan: qword-aligned, +0x19c == +0x1a0 == pow2 in [256..65536], +0x1ac in {2,4,8,16}).
  • Two independent live SP instances (fork L/R, vptr 0x1824ac210 / 0x1824ac7a8, vtbl-slot families 0x18052xxx = SpectralProcessor) BOTH report: +0x19c N0 = 2048, +0x1a0 = 2048 (=N0, since sr==44100 => 2^floor(log2(1))=1) +0x1a4 winscale = 0.49999988 (Hann 0.5) / 0.66666639 (2/3 window) +0x1ac oversample = 4
  • N0=2048 > 1024 => twiddle goes the ANGLE-RAMP branch of FUN_180039b00 (buf[k]=k*(2π/N) then sin/cos via thunk 1801de760 / thunk 1801e3f20), and cplx-stage loader FUN_18014ec20 re-derives re/im (Dekker-split + Cody-Waite).
  • Implication for reimpl: for 44.1k we MUST reproduce Cody-Waite sin/cos (1/π=0.31830988618379069, magic 6755399441055744, π_hi/π_lo reduction, poly -1/3!..-1/15!), NOT the static 1024-table path. Soothing does NOT use the static sin-table for its main FFT at 44.1k.
  • Method: rtone.py (single render, wait for out growth, scan), rtver.py (field verify). Old soothe_rt.bin is module-only range => heap objects absent (why vptr-scan found 0).

B.1 — C++ реализация DSP-ядра (2026-08-17) — IN PROGRESS

  • dsp/fft_plan.hpp/.cpp — radix-2/4/8 планировщик (из гигантов FUN_18002f980 и др.)
  • dsp/fft_stage.hpp/.cpp — stage kernels FUN_18000bfc0/18000c5e0 (cplx-mul + accum)
  • dsp/fft.hpp/.cpp — полный цикл (plan→twiddle→execute)
  • dsp/spectral.hpp/.cpp — SpectralProcessor (STFT + детектор + mask application)
  • dsp/filter.hpp/.cpp — FilterGraph, DigitalFilter (M10)
  • dsp/detect.hpp/.cpp — детектор резонансов (envelope + peak suppression)
  • dsp/ms.hpp/.cpp — mid/side encode/decode (M8)
  • dsp/cody_waite.hpp — sin/cos (FUN_1801de760/1801e3f20)
  • dsp/phase_table.hpp/.cpp — sin-таблица 1024
  • dsp/twiddle_builder.cpp — FUN_180039b00 (ветка N>1024)
  • dsp/CMakeLists.txt — сборка libsoothe2_dsp.so
  • dsp/harness.cpp — WAV16→render→WAV24 (stereo+M8)

B.2 — Сборка и верификация (2026-08-17) — IN PROGRESS

  • libsoothe2_dsp.so — собрана
  • harness — работает с WAV16→WAV24
  • Fixed: WAV read aliasing bug (int16→float conversion)
  • Fixed: In-place processing bug (separate input/output buffers)
  • Added: Spectral detector with envelope + peak suppression
  • TODO: verify_bit_exact.py — прогон всех свипов, побайтовое сравнение int24 PCM
  • TODO: dsp_notes.md — документация (карта функция↔смысл, константы, LUT)

B.3 — Осталось сделать

  1. Twiddle-loader FUN_18014ec20 — декомпил есть, нужна транскрипция (Dekker-split + Cody-Waite)
  2. Bit-reverse permutation — FUN_18003b6c0 с XOR-маской DAT_181c5e4e0
  3. C++ транскрипция — переписать fft.cpp с точным порядком FP-операций из декомпиляции
  4. Верификация — подать тон/синусоиду в тот же путь, что и Reaper-рендер

B.4 — Метрики на burst500 (2026-08-17) — BIG JUMP

  • Интеграция: build_twiddle в fft.cpp теперь через soothe::twiddle_load (sincos_single, Cody-Waite, 7-term poly); удалён дубль init_plan (конфликт с fft_plan.cpp), добавлен <vector>.
  • Stage-ядра (fft_stage.cpp переписан: cplx_mul / stage_complex / stage_double = scalar mul FUN_180008500) — починили фазу: corr 0.479 → +0.995, SNR 5.2 → +19.8 dB (shift 0), амплитуда diff ±0.06 dB (ref 26.07, ours 26.13 dBFS).
  • Детектор (смена модели): убрана bell-boost (была НЕВЕРНА); маска soothe = чисто уровнезависимый floor. Тон 258 Гц (громче) режется сильнее (−6.2 дБ), 500 Гц (3.5 дБ), выше 1 кГц ≈ 0 — региональный уровень (RMS по окну 16 бинов, калибровка −26.1 дБFS), mask = 10^(1.041·depth·floor(level)/20).
  • Итог: ref 26.07 / ours 26.13 dBFS, corr 0.99475, SNR 19.80 dB, diff 0.065 dB.

B.5 — Детектор ДЕШИФРОВАН в soothe_mem.bin (2026-08-17) — KEY

  • Тело FUN_180535880/180536f90 в дампе = реальный дешифрованный SSE-код (movsd/cvtpd2ps, загрузка параметров из param_3, цикл по буферу), а НЕ PACE-стаб.
  • Вызов dispatch-shim 0x180001d00: mov [0x1826159a0],%rax; jmp *0x182616008[rax*8]; сейчас idx=4 → 0x180009860 (тонкая обёртка e8 rel32) → реальная FUN_180040d40.
  • Dispatch-таблица 0x182616008: 223 разрешённых записи; реальные цели в .text 0x18000a160..0x180074740 (13+5+8+6 функций по страницам), высокие 0x1817b900/181a69090/... = направляющие, не алгоритм.
  • КРИТИЧНО: этих функций НЕТ в fun_map.txt/decomp_funs.txt (прежний Ghidra-анализ был на зашифрованном .vst3 и не развязал dispatch). .vst3 на диске (PE32+ 39.1M, 12 секций, base 0x180000000) СОВПАДАЕТ с дампом по байтам в .text (проверено для 0x40d40/0x535880) фи(x) → декомпиляция дампа = декомпиляция реального кода.
  • План: разметить таблицу → извлечь байты функций 0x18004xxxx (+0x18000a160..0x180074740) → objdump 0x180000000-adjust → транскрипция в dsp/detect.cpp → верификация SNR 19.8 дБ.
  • Непустые ранее известные ядра (0x180040cc0..0x180041700) = 8 функций = полный алгоритм детектора.

B.6 — ДЕТЕКТОР ПОЛНОСТЬЮ ДЕШИФРОВАН (2026-08-17) — dispatch-модель ИСПРАВЛЕНА

  • ИСПРАВЛЕНИЕ dispatch: у КАЖДОГО стаба СВОЯ таблица (не одна общая 0x182616008). Все стабы читают ОДИН глобальный индекс DAT_1826159a0 (в дампе = 4, константа), но каждый берёт свой слот из своей таблицы (RIP-rel 48 63 05/4c 8d 15/41 ff 24 c2). /tmp/dispatch_map.txt (223 записи одной таблицы) — УСТАРЕЛ для вызовов детектора.
  • Карта стабов FUN_180535880 (float-детектор): 0x180001d00→tbl0x1826160080x180040d40 (AVX-memset); 0x1800019a0→tbl0x182615b880x1800042000x181a63fe0 (fill float N штук значением из xmm0: rcx=bits, rdx=dst, r8d=N; ветка больших через 0x180017580); 0x1800021e0→tbl0x1826166880x1800130e0; 0x1800018b0→tbl0x182615a480x1800030400x181a557c0; 0x180001af0→tbl0x182615d480x180005a20; 0x180001dc0→tbl0x1826161080x18000ad60 (комплексное умножение vfmaddsub213ps); 0x180001fd0→tbl0x1826163c80x18000f620 (векторная FMA-аккумуляция out[i]=out[i]+xmm1*in[i], AVX2, развёртка по 8); 0x180001e20→tbl0x1826161880x18000b8200x181a775a0.
  • FUN_180536f90 = DOUBLE-БЛИЗНЕЦ FUN_180535880 (тот же 3-итерационный цикл, movsd/shl 4 против movss/shl 3): 0x180001a300x180004d60; 0x1800022d00x1800153000x180040d40; 0x180001e800x18000c7e0; 0x1800019100x1800034c00x181a62d00; 0x180001ac00x180005720; 0x180001e500x18000bba0; 0x1800021500x180012320 (double-FMA-аккум); 0x180001f400x18000df200x181a78060.
  • Алгоритм 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+N8, rbp=r15+2N8. Цикл i=0..2:
    • i=0: memset r15 (N qword) значением (A0,0); memset r12 (N qword) значением (B0,0);
    • i=1: rbx=r15+3N*8; fill rbx (N float) константой 0x1824c3ea4; затем 0x1800021e0(rbx,r9,this,N), 0x1800018b0(this,N), 0x180001af0(this,rbp,N);
    • i=2: 0x180001dc0(this,rbp,rbp,N) = комплексное умножение rbp на самого себя;
    • (после каждого i): rbp += A[i]*r15 (0x180001fd0, 2N float) и rbp += B[i]*r12;
    • после цикла: 0x180001e20(r12,r15,this,N).
  • Ключевые ядра (реальные .text/0x181a)*: 0x180040d40=AVX memset по qword-паттерну (798б); 0x181a63fe0=fill N*4 байт широковещательным 32-бит значением (AVX2, up to 0x8000); 0x18000f620=FMA-аккум (scalar+ymm+tail, возврат -8/-6 при null/0); 0x18000ad60=комплексное умножение (vfmaddsub213ps, цепочки xmm/ymm, маска-зоны 0x20 байт); 0x180005a20=обёртка больших chunk`ов (0x7fffffe0 по 8байт) вокруг 0x18001a5a0.
  • Конечные артефакты: /tmp/det/det_535880.bin/.dis (1668б, 435 стр), det_536f90.bin/.dis (1935б, 582 стр), r_*/f_*/p_* для всех ядер, sizes.json.
  • Следующий шаг: транскрипция обоих близнецов в dsp/detect.cpp (или dsp/detector.cpp) с точным порядком float/double операций + комплексного умножения rbp; верификация SNR 19.8 дБ.

B.7 — Коэффициенты детектора/внутренние моды band (2026-08-18) — ДЕШИФРОВАНО

  • Ghidra-путь Layer2: analyzeHeadless ... -process soothe_mem.bin -noanalysis -scriptPath D -postScript X.java (~60-90 с/запуск), -noanalysis ОК; image_base=0, но block ram=[0x180000000,0x186f48fff] => адреса = RVA; A(v)=v (не минус base). Старые .py не работают (нет PyGhidra) → только .java. Побочный баг: скрипты в одной папке перестают компилироваться после одной правки (OSGi кэш по пути) — обход: новый каталог на скрипт. Материалы: /tmp/opencode/ghidra_out/*.java+qmode.txt/q2.txt/q3.txt/ consts.txt/swtab.txt/scanaddr.txt, копии в /tmp/det/ghidra/.
  • band-структура (подтверждена сеттерами): +0x800=int mode (сеттер 0x18052d206: пишет XML value), +0x804=freq float (0x18053763b), +0x808=sens float, +0x80c=Q float (0x180537615), +0x814=order count (2/3/5), +0x818/+0x820=ptr, +0x819/+0x81a=byte флаги.
  • Писатель коэффициентов FUN_1805316e0 (0x1805316e0..0x180532f3d): switch на [band+0x800], jump-table 0x180532f80 (база LEA 0x180000000, 17 случаев 0..16, >0x10→JA default), каждый case: order в +0x814, вызов генератора, memcpy A/B→band.A/B (0x181132884, len [+0x10]<<3). case1 (XML mode=1)=FUN_1805343e0(fs,freq,Q=0.707)фиксированный Q, +0x80c НЕ читается! case8 (default)=FUN_180533ec0(fs,freq,Q=[+0x80c],10^(sens/20)) — полноценный RBJ bell, Q из поля. Случаи 0/2/3/4/9..16 — другие типы (shelf/notch с константами 0.54/0.51/0.6/1.31/2.56...).
  • q-фактор из +0x80c на старте (0x180531771..17f1): 2q²; (2q²+1)/2q²; ·2; ²·0.251; (cf4); +XMM6; (cd0); ·100000; (cc4 on 2.0); /100000 → участвует в клипинге частоты и fVar16.
  • Генераторы (decomp3.txt): FUN_1805343e0 w=1/FUN_181a14cfa((freq·π)/fs), k=(1/Q)·w, a=1/(k+1+w²), A=[a,2a,a], B=[1,(1w²)·2a,(1k+w²)·a]; FUN_180533ec0 RBJ bell с gain (f=(float)p5, w0=clamp·2π/fs, alpha=(cos·0.5)/Q, a0=alpha·f+1, b0=alpha/f+1, b1=s·−0.5); FUN_180534300 (3rd-order вариант), FUN_180533ff0/180534180 (1-3 order bell/notch).
  • Константы (consts.txt): (float) 0x1824c41e0=2.0, 0x1824c3ea4=1.0, 0x1824c3d3c=0.25, 0x1824c45f8=100000.0, 0x1824c3e00=0.707, 0x1824c3d8c=0.5; (double) 0x1824c43d0=100000.0, 0x1824c40c0=0.707, 0x1824c4698=0.5, 0x1824c4f20=0.0, 0x1824c4270=10.0, 0x1824c42b0=20.0.
  • Матем. хелперы 0x181a14xxx — IAT-стабы (JMP [0x181bab3xx]), цели 0x6fffff... вне дампа (грузит плагин). Семантика FUN_181a14cfa не подтверждена: кандидаты sin/sqrt/tan — проверяется на харнессе против baseline (corr 0.99475/SNR 19.80/diff 0.065).
  • ОТВЕТ НА ВОПРОС (c): XML mode=1 реально исполняет case8 (mode=8/default), НЕ case1. Доказательство — серия dual_b1q_* (тон 500+2000, band1 на 500, sens=12, mode=1): подавление @2000 (red@500≈10.2 насыщена и q-независима) монотонно убывает с q: q=0.1→+5.00 dB, 0.3→+2.73, 0.5→+1.5, 0.7→+0.9, 1.0→+0.47, 1.5→+0.09, 2.0→−0.04, 3.0→−0.06. case1 (фикс Q=0.707) q НЕ читает → исключён. case8 (Q=+0x80c, fvar=0.6=sens/20): delta(2000)=[4.94,1.9,0.85,0.46,0.24,0.11,0.06,0.03] rmse≈0.42, верный знак/направление (завышен при q≥0.5 — модель шире факта). Свободный Q_eff(q): ratio≈0.70.95 при q≤1, → рост при q≥1.5 (Δ<0 — вне bell). Структура FUN_180533ec0 подтверждена. mode4-путь (Q=0.5 фикс, fvar=(q0.707)/2+1) даёт ОТРИЦАТЕЛЬНЫЙ тренд (delta 0.57→−5.45) — несовместим. Вывод: case8-структура (A=[1+α,2s,1−α], B=[1+α2,2s,1−α2], α=p·f, α2=p/f, p=0.5·cos(w0)/Q, fvar≈sens/20 или sin(sens/20)=0.565) — модель формы полосы; fvar-различение sin vs x — на харнессе (Q_eff ratio 0.96/0.70 vs 1.25/0.75 при q=0.1/1.0). Несоответствие формы t1kq_only (D(800)=0.92 при Q=0.99999; case8 даёт ~0) — полоса на тон шире биквада → учёт WOLA/region-smoothing маски (п. B.4) + Q_eff-маппинг — следующий шаг.

B.8 — Форма полосы case8: КОРРЕКЦИЯ (middle=-2c) + freq-axis маска (2026-08-18)

  • ИСПРАВЛЕНИЕ B.7: в FUN_180533ec0 средний коэффициент = -2·cos(w0), НЕ -0.5·sin(w0). В B.7 перепутаны c/s. Правильная форма (проверено, rmse=0.007 dB на t1kq_only1): w0=2π·fc/fs; c=cos(w0); s=sin(w0); p=(s·0.5)/Q; alpha=p·gain; alpha2=p/gain; A=[alpha+1, -2c, 1-alpha]; B=[alpha2+1, -2c, 1-alpha2]; mask(f)=|2·B(e^-jw)/A(e^-jw)|. Свойства: |2B/A|=2 (+6.02 dB) на DC/Nyquist, NOTCH на fc (gain>1), ширина ~1/Q.
  • Freq-path (FUN_180530850): близнец вызывается НАПРЯМУЮ FUN_180535880(out=0x540708, buf=0x5406f8, coeff=case8(fs, 8000.0, Q=1.0), in=0x540718, N=513), N=NFFT/2+1; затем copy 0x540708→0x5406f8 (0x1805355d0) и ×= warp-axis 0x5406a8 (0x18052d990). warp-axis 0x5406a8[i] = x/(1+x/f9), x=f_bin/2000 (f_bin=i·fs/1024), f9=cdc(). => mask(f) = |2·B/A|(z=rotor(входная ось)) · (f/(f+2000)-warp).
  • Level-path (FUN_18056e3e0, per-band): normalize вход × 2π/(os·sr) (0x180006e40 = обычный скалярный mul, НЕ frequency-ramp — проверено декомпиляцией), близнец на буфере 0x400, product активных полос → out. Вход в FUN_180563440 = LUT-рампа [0,1] (шаг 1/1023) из кривой param_1+0x188 → это УРОВНЕВАЯ LUT-маска (B.4), не аудио.
  • Численные согласия: t1kq_only1 rmse=0.007 (Q_eff≈1.39-1.48, gain≈1.19-1.2); t1k_b1f Q_eff≈1.03, gain≈1.39; t1kq_b1f Q_eff≈1.11, gain≈1.29. sens=12→gain≈1.2-1.4 (НЕ 3.98). Глубина вреза = f(уровень входа) → задаётся level-путём; форма (Q_eff, notch) — freq-путём.
  • ОТКРЫТОЕ ПРОТИВОРЕЧИЕ dual_b1q: red@500≈10.2 const, red@2000=15.22→10.16 (q=0.1→10). red2000>red500 при q<1 НЕВОЗМОЖЕН для любой маски-нотча |2B/A|(·warp) (врез=минимум, warp f/(f+K) усиливает cut на 500). Все 4 rotor-мода + warp дают red2000<red500. => В dual есть второй механизм (спектральный наклон/уровень-маска per bin: 2000 Гц выше по f/(f+2000)>0.5 → нормированный уровень выше → глубже крас). Нужен full-харнесс обоих путей.
  • Константы: DAT_1824c4380=8000.0 (double, фикс-freq default-генератора), DAT_1824c45b4=2000.0, DAT_1824c4110=2.0, DAT_1824c3e50=0.87, DAT_1824c3c54=1/1023 (LUT-шаг), 0x180006e40/0x180004720= double/float pointwise scale, 0x1805355d0=copy, 0x18052d990=double pointwise mul.

B.9 — Level-path ЗАМКНУТ: потребитель per-bin весов + warp-ось sigmoid (2026-08-18)

  • ПОТРЕБИТЕЛЬ НАЙДЕН: per-bin веса 0x5406b8/6c8/6d8/6e8 применяются в FUN_180529fe0 (per-block спектральный применитель, 5 каналов, ключевой DSP-цикл). Для каждой полосы:
    1. 0x540678[band] *= (amp/sr)·0x540870 (уровень×sens); 2) IIR-сглаживание спектра (состояния 0x540528/2c04f8/3404f8/4c0528/440510 — per-bin level-трекеры); 3) резонанс полосы в 0x5406f8 (каскад 0x540688-коэфф); 4) 0x5406f8 *= 0x540698 (freq-ось) *= 0x5406a8;
    2. 0x5407c8[band] += 0x5406c8[i]·contrib и += 0x5406e8[i]·contrib (0x180003c40);
    3. накопление с 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 — канонический совместный фит.