19 KiB
19 KiB
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·(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.
Контракт из 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 |
| 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-диапазоне 0x1824c0000–0x182700000) из повторного прогонаDumpFuns.java. Точная double/f32-интерпретация —consts_double.txt(чтение PE напрямую,.datavs.rdataчерез RVA). - A.3 ОКНО — найден:
FUN_1805356f0генерирует Hannw(i)=0.5·(1−cos(2πi/N))(double 2π @0x1824c4e00/4248, 1.0 @4da0/4140, 0.5 @4c90/4050; хвост черезcos, чётные пары через SIMDdivpd+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<<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·X−90)·0.115129255),0.115129255=1/(20·log10e),−6.907755=−ln1000,0.707/0.51/0.54/0.001, маппинг резонанса(x−0.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).
Открытые вопросы
- Где реальный DSP-STFT FFT (не Vorbis/не внешний lib)? Искать большие функции с butterfly/стадиями
в 18052*/18053* или через трассировку вызовов от
processBlockспектрального процессора. - Окна/oversample детали — из декомпиляции (A.6) или требуется доп. замеры.
- Стерео-верификация (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 - factorwithparam_5+1depth; build stage plan inparam_1+0x78array. - 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]!=0call giantFUN_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_182616800into scratch byFUN_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): normalizeDAT_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_1826159a0written 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 fromFUN_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_1800025e0slot @1826159e8; float maxFUN_1800104e0slot @1826164a0;180010860= vmax/vmin with const. - Decompiler entry-point trick:
getFunctionAtmisses overlapping/obfuscated entries => usegetFunctionContaining(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_18014ec20thunk ->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).
- FUN_18000bfc0 (complex stage, slot @182616208): loop 0x40: load-twiddle(re,im) ;
- 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_181ba94b0from 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).