Files
soothe2-re/roadmap.md
T

17 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.